diff --git a/aws/aws-mcp-cli-commands/build.gradle.kts b/aws/aws-mcp-cli-commands/build.gradle.kts new file mode 100644 index 000000000..265d78a59 --- /dev/null +++ b/aws/aws-mcp-cli-commands/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("smithy-java.module-conventions") +} + +description = "This module produces service bundles for AWS services" + +extra["displayName"] = "Smithy :: Java :: AWS :: Service Bundler" +extra["moduleName"] = "software.amazon.smithy.java.aws.mcp.cli.commands" + +dependencies { + implementation(project(":mcp:mcp-bundle-api")) + implementation(libs.smithy.model) + implementation(libs.picocli) + implementation(project(":aws:aws-mcp-types")) + // we need to be able to resolve the sigv4 and protocol traits + implementation(libs.smithy.aws.traits) + implementation(project(":mcp:mcp-cli-api")) + implementation(project(":aws:aws-service-bundler")) + + testImplementation(libs.aws.sdk.auth) +} diff --git a/aws/aws-mcp-cli-commands/src/main/java/software/amazon/smithy/java/aws/mcp/cli/commands/AddAwsServiceBundle.java b/aws/aws-mcp-cli-commands/src/main/java/software/amazon/smithy/java/aws/mcp/cli/commands/AddAwsServiceBundle.java new file mode 100644 index 000000000..e25b4cb2d --- /dev/null +++ b/aws/aws-mcp-cli-commands/src/main/java/software/amazon/smithy/java/aws/mcp/cli/commands/AddAwsServiceBundle.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.aws.mcp.cli.commands; + +import java.util.Set; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import software.amazon.smithy.java.aws.servicebundle.bundler.AwsServiceBundler; +import software.amazon.smithy.java.mcp.cli.AbstractAddBundle; +import software.amazon.smithy.java.mcp.cli.CliBundle; +import software.amazon.smithy.java.mcp.cli.model.McpBundleConfig; +import software.amazon.smithy.java.mcp.cli.model.SmithyModeledBundleConfig; +import software.amazon.smithy.mcp.bundle.api.model.Bundle; + +@Command(name = "add-aws-bundle") +public class AddAwsServiceBundle extends AbstractAddBundle { + + @Option(names = "--overwrite", + description = "Overwrite existing config", + defaultValue = "false") + protected boolean overwrite; + + @Option(names = {"-n", "--name"}, description = "Name of the AWS Service.", required = true) + protected String awsServiceName; + + @Option(names = {"-a", "--allowed-apis"}, description = "List of APIs to expose in the MCP server") + protected Set allowedApis; + + @Option(names = {"-b", "--blocked-apis"}, description = "List of APIs to hide in the MCP server") + protected Set blockedApis; + + @Override + protected CliBundle getNewToolConfig() { + var bundle = new AwsServiceBundler(awsServiceName).bundle(); + + var bundleConfig = McpBundleConfig.builder() + .smithyModeled(SmithyModeledBundleConfig.builder() + .name(awsServiceName) + .bundleLocation(getBundleFileLocation()) + .build()) + .build(); + return new CliBundle(Bundle.builder().smithyBundle(bundle).build(), bundleConfig); + } + + @Override + protected String getToolBundleName() { + return awsServiceName; + } + + @Override + protected boolean canOverwrite() { + return overwrite; + } + + @Override + protected Set allowedTools() { + return allowedApis; + } + + @Override + protected Set blockedTools() { + return blockedApis; + } +} diff --git a/aws/aws-mcp-cli-commands/src/main/resources/META-INF/services/software.amazon.smithy.java.mcp.cli.ConfigurationCommand b/aws/aws-mcp-cli-commands/src/main/resources/META-INF/services/software.amazon.smithy.java.mcp.cli.ConfigurationCommand new file mode 100644 index 000000000..6f9e77d75 --- /dev/null +++ b/aws/aws-mcp-cli-commands/src/main/resources/META-INF/services/software.amazon.smithy.java.mcp.cli.ConfigurationCommand @@ -0,0 +1 @@ +software.amazon.smithy.java.aws.mcp.cli.commands.AddAwsServiceBundle \ No newline at end of file diff --git a/aws/aws-service-bundle/build.gradle.kts b/aws/aws-service-bundle/build.gradle.kts index d812c8be8..16fc4a7dd 100644 --- a/aws/aws-service-bundle/build.gradle.kts +++ b/aws/aws-service-bundle/build.gradle.kts @@ -13,7 +13,7 @@ dependencies { api(project(":auth-api")) implementation(project(":aws:aws-sigv4")) implementation(project(":aws:client:aws-client-core")) - implementation(project(":model-bundler:bundle-api")) + implementation(project(":mcp:mcp-bundle-api")) implementation(project(":aws:sdkv2:aws-sdkv2-auth")) implementation(libs.aws.sdk.auth) } diff --git a/aws/aws-service-bundle/src/main/java/software/amazon/smithy/java/aws/servicebundle/provider/AwsServiceBundle.java b/aws/aws-service-bundle/src/main/java/software/amazon/smithy/java/aws/servicebundle/provider/AwsServiceBundle.java index b0150cacb..6b18976a3 100644 --- a/aws/aws-service-bundle/src/main/java/software/amazon/smithy/java/aws/servicebundle/provider/AwsServiceBundle.java +++ b/aws/aws-service-bundle/src/main/java/software/amazon/smithy/java/aws/servicebundle/provider/AwsServiceBundle.java @@ -13,9 +13,13 @@ import software.amazon.smithy.java.aws.client.auth.scheme.sigv4.SigV4AuthScheme; import software.amazon.smithy.java.aws.client.core.settings.RegionSetting; import software.amazon.smithy.java.aws.sdkv2.auth.SdkCredentialsResolver; +import software.amazon.smithy.java.client.core.Client; +import software.amazon.smithy.java.client.core.ClientConfig; import software.amazon.smithy.java.client.core.RequestOverrideConfig; import software.amazon.smithy.java.client.core.auth.scheme.AuthScheme; import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; +import software.amazon.smithy.java.client.core.interceptors.CallHook; +import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor; import software.amazon.smithy.java.core.serde.document.Document; import software.amazon.smithy.modelbundle.api.BundlePlugin; import software.amazon.smithy.modelbundle.api.StaticAuthSchemeResolver; @@ -30,16 +34,36 @@ final class AwsServiceBundle implements BundlePlugin { } @Override - public RequestOverrideConfig.Builder buildOverride(Document args) { - var input = args.asShape(PreRequest.builder()); - var endpoint = URI.create(Objects.requireNonNull(serviceMetadata.getEndpoints().get(input.getAwsRegion()), - "no endpoint for region " + input.getAwsRegion())); - var identityResolver = new SdkCredentialsResolver(ProfileCredentialsProvider.create(input.getAwsProfileName())); - return RequestOverrideConfig.builder() - .putConfig(RegionSetting.REGION, input.getAwsRegion()) - .endpointResolver(EndpointResolver.staticEndpoint(endpoint)) - .addIdentityResolver(identityResolver) - .authSchemeResolver(StaticAuthSchemeResolver.getInstance()) - .putSupportedAuthSchemes(StaticAuthSchemeResolver.staticScheme(authScheme)); + public > B configureClient(B clientBuilder) { + clientBuilder.addInterceptor(new AwsServiceClientInterceptor(serviceMetadata, authScheme)); + clientBuilder.endpointResolver(EndpointResolver.staticEndpoint("http://localhost")); + return clientBuilder; + } + + private record AwsServiceClientInterceptor(AwsServiceMetadata serviceMetadata, AuthScheme authScheme) + implements ClientInterceptor { + + @Override + public ClientConfig modifyBeforeCall(CallHook hook) { + if (!(hook.input() instanceof Document d)) { + throw new IllegalArgumentException("Input must be a Document"); + } + var input = d.asShape(PreRequest.builder()); + + var endpoint = URI.create(Objects.requireNonNull(serviceMetadata.getEndpoints().get(input.getAwsRegion()), + "no endpoint for region " + input.getAwsRegion())); + + var identityResolver = + new SdkCredentialsResolver(ProfileCredentialsProvider.create(input.getAwsProfileName())); + + return hook.config() + .withRequestOverride(RequestOverrideConfig.builder() + .putConfig(RegionSetting.REGION, input.getAwsRegion()) + .endpointResolver(EndpointResolver.staticEndpoint(endpoint)) + .addIdentityResolver(identityResolver) + .authSchemeResolver(StaticAuthSchemeResolver.getInstance()) + .putSupportedAuthSchemes(StaticAuthSchemeResolver.staticScheme(authScheme)) + .build()); + } } } diff --git a/aws/aws-service-bundler/build.gradle.kts b/aws/aws-service-bundler/build.gradle.kts index 031156a34..4691b0ad7 100644 --- a/aws/aws-service-bundler/build.gradle.kts +++ b/aws/aws-service-bundler/build.gradle.kts @@ -8,7 +8,7 @@ extra["displayName"] = "Smithy :: Java :: AWS :: Service Bundler" extra["moduleName"] = "software.amazon.smithy.java.aws.servicebundle.bundler" dependencies { - implementation(project(":model-bundler:bundle-api")) + implementation(project(":model-bundle:model-bundle-api")) implementation(libs.smithy.model) implementation(project(":aws:aws-mcp-types")) // we need to be able to resolve the sigv4 and protocol traits diff --git a/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundler.java b/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundler.java index 7135ca49a..69df4c46a 100644 --- a/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundler.java +++ b/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundler.java @@ -17,7 +17,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import software.amazon.smithy.aws.traits.auth.SigV4Trait; import software.amazon.smithy.awsmcp.model.AwsServiceMetadata; import software.amazon.smithy.awsmcp.model.PreRequest; @@ -29,16 +28,16 @@ import software.amazon.smithy.model.shapes.ModelSerializer; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.EndpointTrait; -import software.amazon.smithy.modelbundle.api.Bundler; -import software.amazon.smithy.modelbundle.api.model.Bundle; -import software.amazon.smithy.modelbundle.api.model.GenericArguments; -import software.amazon.smithy.modelbundle.api.model.Model; +import software.amazon.smithy.modelbundle.api.ModelBundler; +import software.amazon.smithy.modelbundle.api.model.AdditionalInput; +import software.amazon.smithy.modelbundle.api.model.SmithyBundle; -final class AwsServiceBundler implements Bundler { +public final class AwsServiceBundler extends ModelBundler { private static final ShapeId ENDPOINT_TESTS = ShapeId.from("smithy.rules#endpointTests"); // visible for testing static final Map GH_URIS_BY_SERVICE = new HashMap<>(); + static { // line is in the form fooService/service/version/fooService.json try (var models = new BufferedReader(new InputStreamReader( @@ -55,17 +54,17 @@ final class AwsServiceBundler implements Bundler { private final ModelResolver resolver; private final String serviceName; - AwsServiceBundler(String... args) { - this(args[0].toLowerCase(Locale.ROOT), GithubModelResolver.INSTANCE); - } - AwsServiceBundler(String serviceName, ModelResolver resolver) { this.serviceName = serviceName; this.resolver = resolver; } + public AwsServiceBundler(String serviceName) { + this(serviceName, GithubModelResolver.INSTANCE); + } + @Override - public Bundle bundle() { + public SmithyBundle bundle() { try { var modelString = resolver.getModel(serviceName); var model = new ModelAssembler() @@ -92,18 +91,14 @@ public Bundle bundle() { bundle.endpoints(parseEndpoints(trait.toNode().expectObjectNode())); } } - return Bundle.builder() + return SmithyBundle.builder() .config(Document.of(bundle.build())) .configType("aws") .serviceName(model.getServiceShapes().iterator().next().getId().toString()) - .model(Model.builder() - .smithyModel(serializeModel(model)) - .build()) - .requestArguments(GenericArguments.builder() + .model(serializeModel(model)) + .additionalInput(AdditionalInput.builder() .identifier(PreRequest.$ID.toString()) - .model(Model.builder() - .smithyModel(loadModel("/META-INF/smithy/bundle.smithy")) - .build()) + .model(loadModel("/META-INF/smithy/bundle.smithy")) .build()) .build(); } catch (Exception e) { @@ -117,16 +112,6 @@ private static String serializeModel(software.amazon.smithy.model.Model model) { .serialize(model)); } - private static String loadModel(String path) { - try (var reader = new BufferedReader(new InputStreamReader( - Objects.requireNonNull(AwsServiceBundler.class.getResourceAsStream(path)), - StandardCharsets.UTF_8))) { - return reader.lines().collect(Collectors.joining()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - Map parseEndpoints(ObjectNode endpointTests) { var testCases = endpointTests.expectArrayMember("testCases"); var endpoints = new HashMap(); diff --git a/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerFactory.java b/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerFactory.java deleted file mode 100644 index 00558c73c..000000000 --- a/aws/aws-service-bundler/src/main/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.aws.servicebundle.bundler; - -import software.amazon.smithy.modelbundle.api.Bundler; -import software.amazon.smithy.modelbundle.api.BundlerFactory; - -public final class AwsServiceBundlerFactory implements BundlerFactory { - @Override - public String identifier() { - return "aws"; - } - - @Override - public Bundler newBundler(String... args) { - return new AwsServiceBundler(args); - } -} diff --git a/aws/aws-service-bundler/src/main/resources/META-INF/services/software.amazon.smithy.modelbundle.api.BundlerFactory b/aws/aws-service-bundler/src/main/resources/META-INF/services/software.amazon.smithy.modelbundle.api.BundlerFactory deleted file mode 100644 index 3a4b26378..000000000 --- a/aws/aws-service-bundler/src/main/resources/META-INF/services/software.amazon.smithy.modelbundle.api.BundlerFactory +++ /dev/null @@ -1 +0,0 @@ -software.amazon.smithy.java.aws.servicebundle.bundler.AwsServiceBundlerFactory \ No newline at end of file diff --git a/aws/aws-service-bundler/src/test/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerTest.java b/aws/aws-service-bundler/src/test/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerTest.java index 5ed64aee1..349d9adaa 100644 --- a/aws/aws-service-bundler/src/test/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerTest.java +++ b/aws/aws-service-bundler/src/test/java/software/amazon/smithy/java/aws/servicebundle/bundler/AwsServiceBundlerTest.java @@ -17,12 +17,13 @@ public class AwsServiceBundlerTest { @Test public void accessAnalyzer() { var bundler = new AwsServiceBundler("accessanalyzer-2019-11-01.json", AwsServiceBundlerTest::getModel); - var bundle = bundler.bundle().getConfig().asShape(AwsServiceMetadata.builder()); + var bundle = bundler.bundle(); + var config = bundle.getConfig().asShape(AwsServiceMetadata.builder()); - assertEquals("access-analyzer", bundle.getSigv4SigningName()); - assertEquals("AccessAnalyzer", bundle.getServiceName()); + assertEquals("access-analyzer", config.getSigv4SigningName()); + assertEquals("AccessAnalyzer", config.getServiceName()); - assertNotEquals(0, bundle.getEndpoints().size()); + assertNotEquals(0, config.getEndpoints().size()); } private static String getModel(String path) { diff --git a/examples/mcp-server/build.gradle.kts b/examples/mcp-server/build.gradle.kts index 7362f2423..6c883cbda 100644 --- a/examples/mcp-server/build.gradle.kts +++ b/examples/mcp-server/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-client-awsjson:$smithyJavaVersion") implementation("software.amazon.smithy.java:aws-service-bundle:$smithyJavaVersion") + implementation("software.amazon.smithy.java:mcp-bundle-api:$smithyJavaVersion") } // Add generated Java files to the main sourceSet diff --git a/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/BundleMCPServerExample.java b/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/BundleMCPServerExample.java index 0c60fbd28..e4986f9ff 100644 --- a/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/BundleMCPServerExample.java +++ b/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/BundleMCPServerExample.java @@ -1,22 +1,20 @@ package software.amazon.smithy.java.example.server.mcp; import software.amazon.smithy.java.json.JsonCodec; -import software.amazon.smithy.java.server.ProxyService; -import software.amazon.smithy.java.server.mcp.MCPServer; -import software.amazon.smithy.modelbundle.api.model.Bundle; +import software.amazon.smithy.java.mcp.server.McpServer; +import software.amazon.smithy.mcp.bundle.api.model.Bundle; +import software.amazon.smithy.mcp.bundle.api.McpBundles; import java.util.Objects; public final class BundleMCPServerExample { public static void main(String[] args) throws Exception { try { - var mcpServer = MCPServer.builder() + var bundle = loadBundle("dynamodb.json"); + var mcpServer = McpServer.builder() .stdio() .name("smithy-mcp-server") - .addService( - ProxyService.builder() - .bundle(loadBundle("dynamodb.json")) - .build()) + .addService(McpBundles.getService(bundle.getValue())) .build(); mcpServer.start(); diff --git a/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/MCPServerExample.java b/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/MCPServerExample.java index 710a1281a..f29a29bdc 100644 --- a/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/MCPServerExample.java +++ b/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/MCPServerExample.java @@ -3,7 +3,7 @@ import software.amazon.smithy.java.example.server.mcp.operations.GetCodingStatistics; import software.amazon.smithy.java.example.server.mcp.operations.GetEmployeeDetails; import software.amazon.smithy.java.example.server.mcp.service.EmployeeService; -import software.amazon.smithy.java.server.mcp.MCPServer; +import software.amazon.smithy.java.mcp.server.McpServer; public class MCPServerExample { @@ -13,7 +13,7 @@ public static void main(String[] args) { .addGetEmployeeDetailsOperation(new GetEmployeeDetails()) .build(); - var mcpServer = MCPServer.builder() + var mcpServer = McpServer.builder() .stdio() .name("smithy-mcp-server") .addService(service) diff --git a/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/ProxyMCPExample.java b/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/ProxyMCPExample.java index 9e4676041..771fd5d76 100644 --- a/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/ProxyMCPExample.java +++ b/examples/mcp-server/src/main/java/software/amazon/smithy/java/example/server/mcp/ProxyMCPExample.java @@ -1,13 +1,12 @@ package software.amazon.smithy.java.example.server.mcp; -import software.amazon.smithy.java.client.core.auth.scheme.AuthScheme; import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver; import software.amazon.smithy.java.example.server.mcp.operations.GetCodingStatistics; import software.amazon.smithy.java.example.server.mcp.operations.GetEmployeeDetails; import software.amazon.smithy.java.example.server.mcp.service.EmployeeService; +import software.amazon.smithy.java.mcp.server.McpServer; import software.amazon.smithy.java.server.ProxyService; import software.amazon.smithy.java.server.Server; -import software.amazon.smithy.java.server.mcp.MCPServer; import software.amazon.smithy.model.Model; public class ProxyMCPExample { @@ -39,7 +38,7 @@ public static void main(String[] args) { .proxyEndpoint("http://localhost:8080") .build(); - var mcpServer = MCPServer.builder() + var mcpServer = McpServer.builder() .stdio() .name("smithy-mcp-server") .addService(mcpService) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33fa5c628..2593f89fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ smithy-protocol-tests = { module = "software.amazon.smithy:smithy-protocol-tests smithy-validation-model = { module = "software.amazon.smithy:smithy-validation-model", version.ref = "smithy" } smithy-jmespath = { module = "software.amazon.smithy:smithy-jmespath", version.ref = "smithy" } smithy-waiters = { module = "software.amazon.smithy:smithy-waiters", version.ref = "smithy" } +smithy-utils = { module = "software.amazon.smithy:smithy-utils", version.ref = "smithy" } # AWS SDK for Java V2 adapters. aws-sdk-retries-spi = {module = "software.amazon.awssdk:retries-spi", version.ref = "aws-sdk"} diff --git a/mcp/mcp-bundle-api/build.gradle.kts b/mcp/mcp-bundle-api/build.gradle.kts new file mode 100644 index 000000000..ede0fed45 --- /dev/null +++ b/mcp/mcp-bundle-api/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + application + id("smithy-java.module-conventions") + id("software.amazon.smithy.gradle.smithy-base") +} + +description = "This module implements the mcp-bundler utility" + +extra["displayName"] = "Smithy :: Java :: MCP Bundler" +extra["moduleName"] = "software.amazon.smithy.java.mcp.bundle.api" + +dependencies { + smithyBuild(project(":codegen:plugins:types-codegen")) + + implementation(project(":core")) + implementation(libs.smithy.model) + api(project(":server:server-api")) + api(project(":model-bundle:model-bundle-api")) + api(project(":client:client-core")) + api(project(":dynamic-schemas")) + implementation(project(":server:server-proxy")) +} + +afterEvaluate { + val typePath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-type-codegen") + sourceSets { + main { + java { + srcDir(typePath) + include("software/**") + } + resources { + srcDir(typePath) + include("META-INF/**") + } + } + } +} + +tasks.named("compileJava") { + dependsOn("smithyBuild") +} + +// Needed because sources-jar needs to run after smithy-build is done +tasks.sourcesJar { + mustRunAfter("compileJava") +} + +tasks.processResources { + dependsOn("compileJava") +} diff --git a/model-bundler/bundle-api/license.txt b/mcp/mcp-bundle-api/license.txt similarity index 100% rename from model-bundler/bundle-api/license.txt rename to mcp/mcp-bundle-api/license.txt diff --git a/mcp/mcp-bundle-api/smithy-build.json b/mcp/mcp-bundle-api/smithy-build.json new file mode 100644 index 000000000..dcdd30843 --- /dev/null +++ b/mcp/mcp-bundle-api/smithy-build.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "plugins": { + "java-type-codegen": { + "namespace": "software.amazon.smithy.mcp.bundle.api", + "headerFile": "license.txt", + "useExternalTypes": true + } + } +} diff --git a/mcp/mcp-bundle-api/src/main/java/software/amazon/smithy/mcp/bundle/api/McpBundles.java b/mcp/mcp-bundle-api/src/main/java/software/amazon/smithy/mcp/bundle/api/McpBundles.java new file mode 100644 index 000000000..91c658272 --- /dev/null +++ b/mcp/mcp-bundle-api/src/main/java/software/amazon/smithy/mcp/bundle/api/McpBundles.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.mcp.bundle.api; + +import software.amazon.smithy.java.server.Service; +import software.amazon.smithy.mcp.bundle.api.model.Bundle; +import software.amazon.smithy.modelbundle.api.ModelBundles; + +public class McpBundles { + + private McpBundles() {} + + public static Service getService(Bundle bundle) { + return switch (bundle.type()) { + case smithyBundle -> ModelBundles.getService(bundle.getValue()); + default -> throw new IllegalArgumentException("Unsupported bundle type: " + bundle.type()); + }; + } +} diff --git a/mcp/mcp-bundle-api/src/main/resources/META-INF/smithy/manifest b/mcp/mcp-bundle-api/src/main/resources/META-INF/smithy/manifest new file mode 100644 index 000000000..946b7ad4c --- /dev/null +++ b/mcp/mcp-bundle-api/src/main/resources/META-INF/smithy/manifest @@ -0,0 +1 @@ +mcpbundle.smithy \ No newline at end of file diff --git a/mcp/mcp-bundle-api/src/main/resources/META-INF/smithy/mcpbundle.smithy b/mcp/mcp-bundle-api/src/main/resources/META-INF/smithy/mcpbundle.smithy new file mode 100644 index 000000000..e24e213b2 --- /dev/null +++ b/mcp/mcp-bundle-api/src/main/resources/META-INF/smithy/mcpbundle.smithy @@ -0,0 +1,19 @@ +$version: "2" + +namespace software.amazon.smithy.mcp.bundle.api + +use software.amazon.smithy.modelbundle.api#SmithyBundle + +union Bundle { + smithyBundle: SmithyBundle +} + +structure BundleMetadata { + @required + name: String + + @required + description: String + + version: String +} diff --git a/mcp/mcp-cli-api/build.gradle.kts b/mcp/mcp-cli-api/build.gradle.kts new file mode 100644 index 000000000..1948fca5f --- /dev/null +++ b/mcp/mcp-cli-api/build.gradle.kts @@ -0,0 +1,56 @@ +plugins { + id("smithy-java.module-conventions") + id("software.amazon.smithy.gradle.smithy-base") +} + +description = "This module provides a apis for MCP CLI" + +extra["displayName"] = "Smithy :: Java :: MCP CLI API" +extra["moduleName"] = "software.amazon.smithy.mcp.cli.api" + +dependencies { + api(project(":core")) + api(libs.smithy.model) + api(libs.picocli) + api(project(":mcp:mcp-bundle-api")) + implementation(project(":codecs:json-codec")) + implementation(project(":logging")) + smithyBuild(project(":codegen:plugins:types-codegen")) +} + +sourceSets { + main { + java { + srcDir("model") + } + } +} + +afterEvaluate { + val typePath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-type-codegen") + sourceSets { + main { + java { + srcDir(typePath) + include("software/**") + } + resources { + srcDir(typePath) + include("META-INF/**") + } + } + } +} + +tasks.named("compileJava") { + dependsOn("smithyBuild") +} + +// Needed because sources-jar needs to run after smithy-build is done +tasks.sourcesJar { + mustRunAfter(tasks.compileJava) +} + +tasks.processResources { + dependsOn(tasks.compileJava) +} diff --git a/model-bundler/bundle-cli/license.txt b/mcp/mcp-cli-api/license.txt similarity index 100% rename from model-bundler/bundle-cli/license.txt rename to mcp/mcp-cli-api/license.txt diff --git a/mcp/mcp-cli-api/model/main.smithy b/mcp/mcp-cli-api/model/main.smithy new file mode 100644 index 000000000..7bf0a03f8 --- /dev/null +++ b/mcp/mcp-cli-api/model/main.smithy @@ -0,0 +1,66 @@ +$version: "2" + +namespace smithy.mcp.cli + +structure Config { + toolBundles: McpBundleConfigs + registries: Registries +} + +map McpBundleConfigs { + key: String + value: McpBundleConfig +} + +union McpBundleConfig { + smithyModeled: SmithyModeledBundleConfig + genericConfig: GenericToolBundleConfig +} + +@mixin +structure CommonToolConfig { + name: String + allowListedTools: ToolNames + blockListedTools: ToolNames +} + +map Registries { + key: String + value: RegistryConfig +} + +union RegistryConfig { + javaRegistry: JavaRegistry +} + +structure JavaRegistry with [CommonRegistryConfig] { + jars: Locations +} + +@mixin +structure CommonRegistryConfig { + name: String +} + +structure SmithyModeledBundleConfig with [CommonToolConfig] { + @required + bundleLocation: Location +} + +list Locations { + member: Location +} + +union Location { + fileLocation: String +} + +structure GenericToolBundleConfig with [CommonToolConfig] { + config: Document +} + +list ToolNames { + member: ToolName +} + +string ToolName diff --git a/mcp/mcp-cli-api/smithy-build.json b/mcp/mcp-cli-api/smithy-build.json new file mode 100644 index 000000000..7c24bf49a --- /dev/null +++ b/mcp/mcp-cli-api/smithy-build.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "plugins": { + "java-type-codegen": { + "namespace": "software.amazon.smithy.java.mcp.cli", + "headerFile": "license.txt", + "useExternalTypes": true + } + } +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/AbstractAddBundle.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/AbstractAddBundle.java new file mode 100644 index 000000000..7f24d0851 --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/AbstractAddBundle.java @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import java.util.Set; +import software.amazon.smithy.java.mcp.cli.model.Config; +import software.amazon.smithy.java.mcp.cli.model.Location; + +/** + * Abstract base class for CLI commands that add tool bundles to the Smithy MCP configuration. + *

+ * Subclasses must implement methods to provide tool bundle configuration details and specify + * whether existing configurations can be overwritten. + * + */ +public abstract class AbstractAddBundle extends SmithyMcpCommand implements ConfigurationCommand { + + @Override + public final void execute(Config config) throws Exception { + if (!canOverwrite() && config.getToolBundles().containsKey(getToolBundleName())) { + throw new IllegalArgumentException("Tool bundle " + getToolBundleName() + + " already exists. Either choose a new name or pass --overwrite to overwrite the existing tool bundle"); + } + var newConfig = getNewToolConfig(); + ConfigUtils.addMcpBundle(config, getToolBundleName(), newConfig); + System.out.println("Added tool bundle " + getToolBundleName()); + } + + protected final Location getBundleFileLocation() { + return Location.builder() + .fileLocation(ConfigUtils.getBundleFileLocation(getToolBundleName()).toString()) + .build(); + } + + /** + * Returns a new tool configuration instance to be added to the Smithy MCP config. + * + * @return A new tool bundle configuration + */ + protected abstract CliBundle getNewToolConfig(); + + /** + * Returns the name under which this tool bundle will be registered. + * + * @return The tool bundle name + */ + protected abstract String getToolBundleName(); + + /** + * Determines whether an existing tool bundle with the same name can be overwritten. + * + * @return true if existing tool bundle can be overwritten, false otherwise + */ + protected abstract boolean canOverwrite(); + + protected abstract Set allowedTools(); + + protected abstract Set blockedTools(); +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/CliBundle.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/CliBundle.java new file mode 100644 index 000000000..ef6a12191 --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/CliBundle.java @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import software.amazon.smithy.java.mcp.cli.model.McpBundleConfig; +import software.amazon.smithy.mcp.bundle.api.model.Bundle; + +//TODO find a better name for this. +public record CliBundle( + Bundle mcpBundle, + McpBundleConfig mcpBundleConfig) {} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigUtils.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigUtils.java new file mode 100644 index 000000000..33e711b9a --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import static software.amazon.smithy.java.io.ByteBufferUtils.getBytes; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Comparator; +import java.util.HashMap; +import java.util.ServiceLoader; +import software.amazon.smithy.java.core.schema.SerializableStruct; +import software.amazon.smithy.java.json.JsonCodec; +import software.amazon.smithy.java.mcp.cli.model.Config; +import software.amazon.smithy.java.mcp.cli.model.McpBundleConfig; +import software.amazon.smithy.mcp.bundle.api.model.Bundle; + +/** + * Utility class for managing Smithy MCP configuration files. + *

+ * This class provides methods for loading, creating, updating, and serializing MCP configurations. + */ +public class ConfigUtils { + + private ConfigUtils() {} + + private static final JsonCodec JSON_CODEC = JsonCodec.builder().build(); + + private static final Path CONFIG_DIR = resolveFromHomeDir(".config", "smithy-mcp"); + private static final Path BUNDLE_DIR = CONFIG_DIR.resolve("bundles"); + private static final Path CONFIG_PATH = CONFIG_DIR.resolve("config.json"); + + private static final DefaultConfigProvider DEFAULT_CONFIG_PROVIDER; + + static { + try { + ensureDirectoryExists(CONFIG_DIR); + ensureDirectoryExists(BUNDLE_DIR); + } catch (IOException e) { + throw new RuntimeException(e); + } + + DEFAULT_CONFIG_PROVIDER = ServiceLoader.load(DefaultConfigProvider.class) + .stream() + .map(ServiceLoader.Provider::get) + .min(Comparator.comparing(DefaultConfigProvider::priority)) + .orElse(new EmptyDefaultConfigProvider()); + } + + private static Path resolveFromHomeDir(String... paths) { + String userHome = System.getProperty("user.home"); + return Paths.get(userHome, paths); + } + + /** + * Ensures the config directory exists. + * + * @throws IOException If there's an error creating directories + */ + private static void ensureDirectoryExists(Path dir) throws IOException { + if (!Files.exists(dir)) { + Files.createDirectories(dir); + } + } + + /** + * Loads an existing config file or creates one if it doesn't exist. + * + * @return The config file + * @throws IOException If there's an error creating directories or the file + */ + public static Config loadOrCreateConfig() throws IOException { + + if (!CONFIG_PATH.toFile().exists()) { + // Create an empty JSON object as the default config + Files.write(CONFIG_PATH, toJson(DEFAULT_CONFIG_PROVIDER.getConfig()), StandardOpenOption.CREATE); + } + + return fromJson(Files.readAllBytes(CONFIG_PATH)); + } + + static Path getBundleFileLocation(String bundleName) { + return BUNDLE_DIR.resolve(bundleName + ".json"); + } + + /** + * Updates the MCP configuration file with the provided configuration. + * + * @param config The configuration to write to the file + * @throws IOException If there's an error writing to the file + */ + public static void updateConfig(Config config) throws IOException { + Files.write(CONFIG_PATH, toJson(config), StandardOpenOption.TRUNCATE_EXISTING); + } + + /** + * Deserializes a Config object from JSON bytes. + * + * @param json The JSON data as a byte array + * @return The deserialized Config object with defaults applied + */ + private static Config fromJson(byte[] json) { + return Config.builder().deserialize(JSON_CODEC.createDeserializer(json)).build(); + } + + private static byte[] toJson(SerializableStruct struct) { + return getBytes(JSON_CODEC.serialize(struct)); + } + + /** + * Adds a new tool bundle configuration to an existing configuration and saves it. + * + * @param existingConfig The existing configuration to update + * @param name The name under which to register the tool bundle + * @param toolBundleConfig The tool bundle configuration to add + * @throws IOException If there's an error writing the updated configuration + */ + private static void addMcpBundleConfig(Config existingConfig, String name, McpBundleConfig toolBundleConfig) + throws IOException { + var existingToolBundles = new HashMap<>(existingConfig.getToolBundles()); + existingToolBundles.put(name, toolBundleConfig); + var newConfig = existingConfig.toBuilder().toolBundles(existingToolBundles).build(); + updateConfig(newConfig); + } + + public static Bundle getMcpBundle(String bundleName) { + try { + return Bundle.builder() + .deserialize(JSON_CODEC.createDeserializer(Files.readAllBytes(getBundleFileLocation(bundleName)))) + .build(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void addMcpBundle(Config config, String toolBundleName, CliBundle mcpBundleConfig) + throws IOException { + var serializedBundle = toJson(mcpBundleConfig.mcpBundle()); + Files.write(getBundleFileLocation(toolBundleName), + serializedBundle, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE); + addMcpBundleConfig(config, toolBundleName, mcpBundleConfig.mcpBundleConfig()); + } +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigurationCommand.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigurationCommand.java new file mode 100644 index 000000000..379d189bd --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigurationCommand.java @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +/** + * Base class for all Smithy MCP CLI configuration commands. + *

+ * This class extends SmithyMcpCommand to provide a common base for all commands + * that modify the MCP configuration. Subclasses should implement the execute method. + */ +public interface ConfigurationCommand { + +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/DefaultConfigProvider.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/DefaultConfigProvider.java new file mode 100644 index 000000000..76da8ed75 --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/DefaultConfigProvider.java @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import software.amazon.smithy.java.mcp.cli.model.Config; + +public interface DefaultConfigProvider { + Config getConfig(); + + int priority(); +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/EmptyDefaultConfigProvider.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/EmptyDefaultConfigProvider.java new file mode 100644 index 000000000..93150a7d8 --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/EmptyDefaultConfigProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import software.amazon.smithy.java.mcp.cli.model.Config; + +public final class EmptyDefaultConfigProvider implements DefaultConfigProvider { + + @Override + public Config getConfig() { + return Config.builder().build(); + } + + @Override + public int priority() { + return Integer.MIN_VALUE; + } +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/Registry.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/Registry.java new file mode 100644 index 000000000..e10c96e53 --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/Registry.java @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import java.util.List; +import software.amazon.smithy.mcp.bundle.api.model.Bundle; +import software.amazon.smithy.mcp.bundle.api.model.BundleMetadata; + +public interface Registry { + + String name(); + + List listMcpBundles(); + + Bundle getMcpBundle(String name); + +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/RegistryUtils.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/RegistryUtils.java new file mode 100644 index 000000000..122dfb5c1 --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/RegistryUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import java.util.Map; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class RegistryUtils { + + private static final Map JAVA_REGISTRIES; + + static { + JAVA_REGISTRIES = ServiceLoader.load(Registry.class) + .stream() + .map(Provider::get) + .collect(Collectors.toMap(Registry::name, Function.identity())); + } + + public static Registry getRegistry(String name) { + if (JAVA_REGISTRIES.containsKey(name)) { + return JAVA_REGISTRIES.get(name); + } + throw new IllegalStateException("No such registry: " + name); + } +} diff --git a/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/SmithyMcpCommand.java b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/SmithyMcpCommand.java new file mode 100644 index 000000000..bb45f7d6a --- /dev/null +++ b/mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/SmithyMcpCommand.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import static software.amazon.smithy.java.mcp.cli.ConfigUtils.loadOrCreateConfig; + +import java.util.concurrent.Callable; +import software.amazon.smithy.java.logging.InternalLogger; +import software.amazon.smithy.java.mcp.cli.model.Config; + +/** + * Base class for all Smithy MCP CLI commands. + *

+ * This class implements the Callable interface to provide a consistent execution pattern + * for all MCP CLI commands. It handles loading the configuration, executing the command, + * and providing appropriate error handling. + */ +public abstract class SmithyMcpCommand implements Callable { + + InternalLogger LOG = InternalLogger.getLogger(SmithyMcpCommand.class); + + @Override + public final Integer call() throws Exception { + try { + var config = loadOrCreateConfig(); + execute(config); + return 0; + } catch (IllegalArgumentException e) { + System.out.println("Invalid input : [" + e.getMessage() + "]"); + return 2; + } catch (Exception e) { + e.printStackTrace(System.out); + return 1; + } + } + + /** + * Execute the command with the provided configuration. + *

+ * Subclasses must implement this method to provide command-specific functionality. + * + * @param config The MCP configuration + * @throws Exception If an error occurs during execution + */ + protected abstract void execute(Config config) throws Exception; +} diff --git a/mcp/mcp-cli/build.gradle.kts b/mcp/mcp-cli/build.gradle.kts new file mode 100644 index 000000000..218e8827c --- /dev/null +++ b/mcp/mcp-cli/build.gradle.kts @@ -0,0 +1,62 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer +import kotlin.jvm.java + +plugins { + id("smithy-java.module-conventions") + alias(libs.plugins.shadow) + application +} + +description = + "MCP Server support" + +extra["displayName"] = "Smithy :: Java :: MCP CLI" +extra["moduleName"] = "software.amazon.smithy.java.mcp.cli" + +dependencies { + implementation(project(":logging")) + implementation(project(":mcp:mcp-server")) + implementation(project(":server:server-proxy")) + implementation(project(":codecs:json-codec")) + implementation(libs.picocli) + api(project(":mcp:mcp-cli-api")) + implementation(libs.smithy.utils) + + // TODO these need to be dynamically loaded + implementation(project(":aws:aws-mcp-cli-commands")) + implementation(project(":aws:aws-service-bundle")) + implementation(project(":aws:client:aws-client-restjson")) + implementation(project(":aws:client:aws-client-awsjson")) +} + +application { + mainClass = "software.amazon.smithy.java.mcp.cli.McpCli" + applicationDefaultJvmArgs = listOf("-Dorg.slf4j.simpleLogger.defaultLogLevel=off") + applicationName = "smithy-mcp" +} + +val generateVersionFile = + tasks.register("generateVersionFile") { + val versionFile = + sourceSets.main + .get() + .output.resourcesDir + ?.resolve("software/amazon/smithy/java/mcp/cli/VERSION")!! + + outputs.file(versionFile) + + doLast { + versionFile.writeText(project.version.toString()) + } + } + +tasks.processResources { + dependsOn(generateVersionFile) +} + +tasks.shadowJar { + mergeServiceFiles() + transform(AppendingTransformer::class.java) { + resource = "META-INF/smithy/manifest" + } +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/McpCli.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/McpCli.java new file mode 100644 index 000000000..a18cbe441 --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/McpCli.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import java.util.List; +import java.util.ServiceLoader; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import software.amazon.smithy.java.mcp.cli.commands.Configure; +import software.amazon.smithy.java.mcp.cli.commands.StartServer; + +/** + * Main entry point for the Smithy MCP Command Line Interface. + *

+ * This class configures and launches the CLI application using Picocli. + * It discovers and registers all available configuration commands and + * sets up the command hierarchy. + */ +@Command(name = "smithy-mcp", versionProvider = VersionProvider.class, mixinStandardHelpOptions = true, + scope = CommandLine.ScopeType.INHERIT) +public class McpCli { + + public static void main(String[] args) { + var configureCommand = new CommandLine(new Configure()); + discoverConfigurationCommands().forEach(configureCommand::addSubcommand); + var commandLine = new CommandLine(new McpCli()) + .addSubcommand(new StartServer()) + .addSubcommand(configureCommand); + commandLine.execute(args); + } + + /** + * Discovers and loads all ConfigurationCommand implementations using the Java ServiceLoader. + * + * @return A list of discovered configuration commands + */ + private static List discoverConfigurationCommands() { + return ServiceLoader.load(ConfigurationCommand.class) + .stream() + .map(ServiceLoader.Provider::get) + .toList(); + } +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/VersionProvider.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/VersionProvider.java new file mode 100644 index 000000000..7c3ccdc76 --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/VersionProvider.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli; + +import picocli.CommandLine; +import software.amazon.smithy.utils.IoUtils; + +/** + * Provides version information for the Smithy MCP CLI. + *

+ * This class implements Picocli's IVersionProvider interface to provide + * version information from a resource file. + */ +public class VersionProvider implements CommandLine.IVersionProvider { + + @Override + public String[] getVersion() throws Exception { + return new String[] {IoUtils.readUtf8Resource(VersionProvider.class, "VERSION").trim()}; + } +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/AddBundle.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/AddBundle.java new file mode 100644 index 000000000..de1e711a2 --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/AddBundle.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli.commands; + +import static picocli.CommandLine.Command; + +import picocli.CommandLine.Option; +import software.amazon.smithy.java.mcp.cli.RegistryUtils; +import software.amazon.smithy.java.mcp.cli.SmithyMcpCommand; +import software.amazon.smithy.java.mcp.cli.model.Config; + +@Command(name = "add-bundle", description = "Downloads and adds a bundle from the MCP registry.") +public class AddBundle extends SmithyMcpCommand { + + @Option(names = {"-r", "--registry"}, + description = "Name of the registry to list the bundles from. If not provided will list tools across all registries.") + String registry; + + @Option(names = {"-n", "--name"}, description = "Name of the MCP Bundle to install.") + String name; + + @Override + protected void execute(Config config) { + if (registry != null && !config.getRegistries().containsKey(registry)) { + throw new IllegalArgumentException("The registry '" + registry + "' does not exist."); + } + RegistryUtils.getRegistry(registry).getMcpBundle(name); + } +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/AddSmithyBundle.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/AddSmithyBundle.java new file mode 100644 index 000000000..ed00e3a75 --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/AddSmithyBundle.java @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli.commands; + +import java.util.Set; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import software.amazon.smithy.java.mcp.cli.AbstractAddBundle; +import software.amazon.smithy.java.mcp.cli.CliBundle; + +/** + * Command to add a Smithy tool bundle to the MCP configuration. + *

+ * This command allows users to add a new Smithy tool bundle to their MCP configuration. + * Currently under development and hidden from the CLI help. + */ +@Command(name = "add-smithy-bundle", description = "Add a smithy bundle.", hidden = true) +//TODO implement and unhide +public class AddSmithyBundle extends AbstractAddBundle { + + @CommandLine.Option(names = "--overwrite", + description = "Overwrite existing config", + defaultValue = "false") + protected boolean overwrite; + + @CommandLine.Option(names = {"-n", "--name"}, description = "Name of the tool bundle.", required = true) + protected String name; + + @Override + protected CliBundle getNewToolConfig() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + protected String getToolBundleName() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + protected boolean canOverwrite() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + protected Set allowedTools() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + @Override + protected Set blockedTools() { + throw new UnsupportedOperationException("Not implemented yet."); + } +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/Configure.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/Configure.java new file mode 100644 index 000000000..b737a40aa --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/Configure.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli.commands; + +import picocli.CommandLine.Command; + +/** + * Command group for configuration-related commands in the Smithy MCP CLI. + *

+ * This class serves as a container for all subcommands related to configuring + * the MCP CLI, such as adding tool bundles. + */ +@Command(name = "configure", description = "Configure the Smithy MCP CLI", subcommands = { + AddSmithyBundle.class, + ListBundles.class, + AddBundle.class +}) +public final class Configure { + +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/ListBundles.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/ListBundles.java new file mode 100644 index 000000000..aef7eae46 --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/ListBundles.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli.commands; + +import static picocli.CommandLine.Command; + +import picocli.CommandLine.Option; +import software.amazon.smithy.java.mcp.cli.RegistryUtils; +import software.amazon.smithy.java.mcp.cli.SmithyMcpCommand; +import software.amazon.smithy.java.mcp.cli.model.Config; + +@Command(name = "list", description = "List all the MCP Bundles available in the Registry") +public class ListBundles extends SmithyMcpCommand { + + //TODO make it non-required later and search across all registries. + @Option(names = {"-r", "--registry"}, + description = "Name of the registry to list the bundles from. If not provided will list tools across all registries.", + required = true) + String registry; + + @Override + protected void execute(Config config) { + if (registry != null && !config.getRegistries().containsKey(registry)) { + throw new IllegalArgumentException("The registry '" + registry + "' does not exist."); + } + RegistryUtils.getRegistry(registry) + .listMcpBundles() + .forEach(bundle -> System.out.println(bundle.getName() + " : " + bundle.getDescription())); + + } +} diff --git a/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/StartServer.java b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/StartServer.java new file mode 100644 index 000000000..20c4261bf --- /dev/null +++ b/mcp/mcp-cli/src/main/java/software/amazon/smithy/java/mcp/cli/commands/StartServer.java @@ -0,0 +1,85 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.mcp.cli.commands; + +import java.util.ArrayList; +import java.util.List; +import picocli.CommandLine.Command; +import picocli.CommandLine.Parameters; +import software.amazon.smithy.java.mcp.cli.ConfigUtils; +import software.amazon.smithy.java.mcp.cli.SmithyMcpCommand; +import software.amazon.smithy.java.mcp.cli.model.Config; +import software.amazon.smithy.java.mcp.cli.model.McpBundleConfig; +import software.amazon.smithy.java.mcp.cli.model.SmithyModeledBundleConfig; +import software.amazon.smithy.java.mcp.server.McpServer; +import software.amazon.smithy.java.server.FilteredService; +import software.amazon.smithy.java.server.OperationFilters; +import software.amazon.smithy.java.server.Service; +import software.amazon.smithy.mcp.bundle.api.McpBundles; + +/** + * Command to start a Smithy MCP server exposing specified tool bundles. + *

+ * This command loads configured tool bundles and starts an MCP server that + * exposes the operations provided by those bundles. The server runs until + * interrupted or terminated. + */ +@Command(name = "start-server", description = "Starts an MCP server.") +public final class StartServer extends SmithyMcpCommand { + + @Parameters(paramLabel = "TOOL_BUNDLES", description = "Name(s) of the Tool Bundles to expose in this MCP Server.") + List toolBundles; + + /** + * Executes the start-server command. + *

+ * Loads the requested tool bundles from configuration, creates appropriate services, + * and starts the MCP server. + * + * @param config The MCP configuration + * @throws IllegalArgumentException If no tool bundles are configured or requested bundles not found + */ + @Override + public void execute(Config config) { + if (!config.hasToolBundles()) { + throw new IllegalArgumentException( + "No Tool Bundles have been configured. Configure one using the configure-tool-bundle command."); + } + List toolBundleConfigs = new ArrayList<>(toolBundles.size()); + for (var toolBundle : toolBundles) { + var toolBundleConfig = config.getToolBundles().get(toolBundle); + if (toolBundleConfig == null) { + throw new IllegalArgumentException("Can't find a configured tool bundle for '" + toolBundle + "'."); + } + toolBundleConfigs.add(toolBundleConfig); + } + var services = new ArrayList(); + for (var toolBundleConfig : toolBundleConfigs) { + switch (toolBundleConfig.type()) { + case smithyModeled -> { + SmithyModeledBundleConfig bundleConfig = toolBundleConfig.getValue(); + Service service = + McpBundles.getService(ConfigUtils.getMcpBundle(bundleConfig.getName())); + if (bundleConfig.hasAllowListedTools() || bundleConfig.hasBlockListedTools()) { + var filter = OperationFilters.allowList(bundleConfig.getAllowListedTools()) + .and(OperationFilters.blockList(bundleConfig.getBlockListedTools())); + service = new FilteredService(service, filter); + } + services.add(service); + } + default -> + throw new IllegalArgumentException("Unknown tool bundle type '" + toolBundleConfig.type() + "'."); + } + } + var mcpServer = McpServer.builder().stdio().addServices(services).name("smithy-mcp-server").build(); + mcpServer.start(); + try { + Thread.currentThread().join(); + } catch (InterruptedException e) { + mcpServer.shutdown().join(); + } + } +} diff --git a/mcp/mcp-schemas/build.gradle.kts b/mcp/mcp-schemas/build.gradle.kts index 2720b9f44..c6a517b67 100644 --- a/mcp/mcp-schemas/build.gradle.kts +++ b/mcp/mcp-schemas/build.gradle.kts @@ -9,9 +9,9 @@ extra["displayName"] = "Smithy :: Java :: MCP Schemas" extra["moduleName"] = "software.amazon.smithy.mcp.schemas" dependencies { - smithyBuild(project(":codegen:plugins:types-codegen")) api(project(":core")) api(libs.smithy.model) + smithyBuild(project(":codegen:plugins:types-codegen")) } sourceSets { @@ -44,9 +44,9 @@ tasks.named("compileJava") { // Needed because sources-jar needs to run after smithy-build is done tasks.sourcesJar { - mustRunAfter("compileJava") + mustRunAfter(tasks.compileJava) } tasks.processResources { - dependsOn("compileJava") + dependsOn(tasks.compileJava) } diff --git a/mcp/mcp-schemas/model/main.smithy b/mcp/mcp-schemas/model/main.smithy index 2ef9e5cf3..29e894807 100644 --- a/mcp/mcp-schemas/model/main.smithy +++ b/mcp/mcp-schemas/model/main.smithy @@ -117,6 +117,12 @@ structure ToolInputSchema { properties: PropertiesMap required: StringList + + @required + additionalProperties: PrimitiveBoolean = false + + @jsonName("$schema") + schema: String = "http://json-schema.org/draft-07/schema#" } map PropertiesMap { diff --git a/mcp/mcp-server/build.gradle.kts b/mcp/mcp-server/build.gradle.kts index aeda54ce4..99bb173b8 100644 --- a/mcp/mcp-server/build.gradle.kts +++ b/mcp/mcp-server/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { implementation(project(":context")) implementation(project(":codecs:json-codec")) implementation(project(":mcp:mcp-schemas")) - implementation(project(":model-bundler:bundle-api")) + implementation(project(":mcp:mcp-bundle-api")) } spotbugs { diff --git a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/server/mcp/MCPServer.java b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServer.java similarity index 79% rename from mcp/mcp-server/src/main/java/software/amazon/smithy/java/server/mcp/MCPServer.java rename to mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServer.java index 995e61384..3c9478411 100644 --- a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/server/mcp/MCPServer.java +++ b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServer.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.java.server.mcp; +package software.amazon.smithy.java.mcp.server; import java.io.InputStream; import java.io.OutputStream; @@ -40,17 +40,17 @@ import software.amazon.smithy.java.server.Operation; import software.amazon.smithy.java.server.Server; import software.amazon.smithy.java.server.Service; -import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.utils.SmithyUnstableApi; @SmithyUnstableApi -public final class MCPServer implements Server { +public final class McpServer implements Server { - private static final InternalLogger LOG = InternalLogger.getLogger(MCPServer.class); + private static final InternalLogger LOG = InternalLogger.getLogger(McpServer.class); private static final JsonCodec CODEC = JsonCodec.builder() .settings(JsonSettings.builder() .serializeTypeInDocuments(false) + .useJsonName(true) .build()) .build(); @@ -60,7 +60,7 @@ public final class MCPServer implements Server { private final OutputStream os; private final String name; - MCPServer(MCPServerBuilder builder) { + McpServer(McpServerBuilder builder) { this.tools = createTools(builder.serviceList); this.is = builder.is; this.os = builder.os; @@ -186,8 +186,7 @@ private static Map createTools(List serviceList) { .name(operationName) .description(createDescription(serviceName, operationName, - schema - .expectTrait(TraitKey.DOCUMENTATION_TRAIT))) + schema)) .inputSchema(createInputSchema(operation.getApiOperation().inputSchema())) .build(); tools.put(operation.name(), new Tool(toolInfo, operation)); @@ -204,22 +203,53 @@ private static ToolInputSchema createInputSchema(Schema schema) { if (member.hasTrait(TraitKey.REQUIRED_TRAIT)) { requiredProperties.add(name); } - var details = PropertyDetails.builder() - .typeMember(member.type().toString()) - .description(member.expectTrait(TraitKey.DOCUMENTATION_TRAIT).getValue()) - .build(); - properties.put(name, details); + // adapt types to json-schema types + // https://json-schema.org/draft-07/schema# + var type = switch (member.type()) { + case BYTE, SHORT, INTEGER, INT_ENUM, LONG, FLOAT, DOUBLE -> "number"; + case ENUM, BLOB -> "string"; + case LIST, SET -> "array"; + case TIMESTAMP -> resolveTimestampType(member.memberTarget()); + case MAP, DOCUMENT, STRUCTURE, UNION -> "object"; + case STRING, BIG_DECIMAL, BIG_INTEGER -> "string"; + case BOOLEAN -> "boolean"; + default -> throw new RuntimeException("unsupported type: " + member.type() + "on member " + member); + }; + var details = PropertyDetails.builder().typeMember(type); + var documentation = member.getTrait(TraitKey.DOCUMENTATION_TRAIT); + if (documentation != null) { + details.description(documentation.getValue()); + } + properties.put(name, details.build()); } return ToolInputSchema.builder().properties(properties).required(requiredProperties).build(); } + private static String resolveTimestampType(Schema schema) { + var trait = schema.getTrait(TraitKey.TIMESTAMP_FORMAT_TRAIT); + if (trait == null) { + // default is epoch-seconds + return "number"; + } + return switch (trait.getFormat()) { + case EPOCH_SECONDS -> "number"; + case DATE_TIME, HTTP_DATE -> "string"; + default -> throw new RuntimeException("unknown timestamp format: " + trait.getFormat()); + }; + } + private static String createDescription( String serviceName, String operationName, - DocumentationTrait documentationTrait + Schema schema ) { - return "This tool invokes %s API of %s .".formatted(operationName, serviceName) + - documentationTrait.getValue(); + var documentationTrait = schema.getTrait(TraitKey.DOCUMENTATION_TRAIT); + if (documentationTrait != null) { + return "This tool invokes %s API of %s.".formatted(operationName, serviceName) + + documentationTrait.getValue(); + } else { + return "This tool invokes %s API of %s.".formatted(operationName, serviceName); + } } @Override @@ -237,7 +267,7 @@ private record Tool(ToolInfo toolInfo, Operation operation) { } - public static MCPServerBuilder builder() { - return new MCPServerBuilder(); + public static McpServerBuilder builder() { + return new McpServerBuilder(); } } diff --git a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/server/mcp/MCPServerBuilder.java b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServerBuilder.java similarity index 66% rename from mcp/mcp-server/src/main/java/software/amazon/smithy/java/server/mcp/MCPServerBuilder.java rename to mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServerBuilder.java index 9c649a2f4..6d64774ba 100644 --- a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/server/mcp/MCPServerBuilder.java +++ b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpServerBuilder.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package software.amazon.smithy.java.server.mcp; +package software.amazon.smithy.java.mcp.server; import java.io.InputStream; import java.io.OutputStream; @@ -16,49 +16,54 @@ import software.amazon.smithy.utils.SmithyUnstableApi; @SmithyUnstableApi -public final class MCPServerBuilder { +public final class McpServerBuilder { InputStream is; OutputStream os; List serviceList = new ArrayList<>(); String name; - MCPServerBuilder() {} + McpServerBuilder() {} - public MCPServerBuilder stdio() { + public McpServerBuilder stdio() { this.is = System.in; this.os = System.out; return this; } - public MCPServerBuilder input(InputStream is) { + public McpServerBuilder input(InputStream is) { this.is = is; return this; } - public MCPServerBuilder output(OutputStream os) { + public McpServerBuilder output(OutputStream os) { this.os = os; return this; } - public MCPServerBuilder name(String name) { + public McpServerBuilder name(String name) { this.name = name; return this; } public Server build() { validate(); - return new MCPServer(this); + return new McpServer(this); } - public MCPServerBuilder addService(Service... service) { + public McpServerBuilder addService(Service... service) { serviceList.addAll(Arrays.asList(service)); return this; } + public McpServerBuilder addServices(List services) { + serviceList.addAll(services); + return this; + } + private void validate() { Objects.requireNonNull(is, "MCP server input stream is required"); - Objects.requireNonNull(is, "MCP server output stream is required"); + Objects.requireNonNull(os, "MCP server output stream is required"); if (serviceList.isEmpty()) { throw new IllegalArgumentException("MCP server requires at least one service"); } diff --git a/model-bundler/bundle-api/build.gradle.kts b/model-bundle/model-bundle-api/build.gradle.kts similarity index 79% rename from model-bundler/bundle-api/build.gradle.kts rename to model-bundle/model-bundle-api/build.gradle.kts index 033a40e99..8b99cd7c0 100644 --- a/model-bundler/bundle-api/build.gradle.kts +++ b/model-bundle/model-bundle-api/build.gradle.kts @@ -4,9 +4,9 @@ plugins { id("software.amazon.smithy.gradle.smithy-base") } -description = "This module implements the model-bundler utility" +description = "This module implements the model-bundle utility" -extra["displayName"] = "Smithy :: Java :: Model Bundler" +extra["displayName"] = "Smithy :: Java :: Model Bundle" extra["moduleName"] = "software.amazon.smithy.java.modelbundle.api" dependencies { @@ -17,6 +17,8 @@ dependencies { api(project(":client:client-auth-api")) api(project(":client:client-core")) api(project(":dynamic-schemas")) + api(project(":server:server-api")) + api(project(":server:server-proxy")) } afterEvaluate { @@ -41,9 +43,9 @@ tasks.named("compileJava") { // Needed because sources-jar needs to run after smithy-build is done tasks.sourcesJar { - mustRunAfter("compileJava") + mustRunAfter(tasks.compileJava) } tasks.processResources { - dependsOn("compileJava") + dependsOn(tasks.compileJava) } diff --git a/model-bundle/model-bundle-api/license.txt b/model-bundle/model-bundle-api/license.txt new file mode 100644 index 000000000..5f97ab495 --- /dev/null +++ b/model-bundle/model-bundle-api/license.txt @@ -0,0 +1,4 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ \ No newline at end of file diff --git a/model-bundler/bundle-api/smithy-build.json b/model-bundle/model-bundle-api/smithy-build.json similarity index 98% rename from model-bundler/bundle-api/smithy-build.json rename to model-bundle/model-bundle-api/smithy-build.json index def776964..5a78c012c 100644 --- a/model-bundler/bundle-api/smithy-build.json +++ b/model-bundle/model-bundle-api/smithy-build.json @@ -6,4 +6,4 @@ "headerFile": "license.txt" } } -} +} \ No newline at end of file diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePlugin.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePlugin.java similarity index 70% rename from model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePlugin.java rename to model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePlugin.java index cdc113d1b..67a8a9f5f 100644 --- a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePlugin.java +++ b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePlugin.java @@ -5,8 +5,8 @@ package software.amazon.smithy.modelbundle.api; +import software.amazon.smithy.java.client.core.Client; import software.amazon.smithy.java.client.core.RequestOverrideConfig; -import software.amazon.smithy.java.core.serde.document.Document; /** * A BundlePlugin applies the settings specified in a {@link software.amazon.smithy.modelbundle.api.model.Bundle} @@ -15,9 +15,7 @@ public interface BundlePlugin { /** * Applies the bundle-specific settings to a client call. - * - * @param args per-request args specified by the bundle, or null if the bundle takes no per-request args * @return a {@link RequestOverrideConfig.Builder} with the settings from the bundle applied */ - RequestOverrideConfig.Builder buildOverride(Document args); + > B configureClient(B clientBuilder); } diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePluginFactory.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePluginFactory.java similarity index 100% rename from model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePluginFactory.java rename to model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlePluginFactory.java diff --git a/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ModelBundler.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ModelBundler.java new file mode 100644 index 000000000..ca20a46b6 --- /dev/null +++ b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ModelBundler.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.modelbundle.api; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.stream.Collectors; +import software.amazon.smithy.modelbundle.api.model.SmithyBundle; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.SmithyUnstableApi; + +@SmithyInternalApi +@SmithyUnstableApi +public abstract class ModelBundler { + + public abstract SmithyBundle bundle(); + + protected static String loadModel(String path) { + try (var reader = new BufferedReader(new InputStreamReader( + Objects.requireNonNull(ModelBundler.class.getResourceAsStream(path)), + StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ModelBundles.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ModelBundles.java new file mode 100644 index 000000000..61c184f0c --- /dev/null +++ b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ModelBundles.java @@ -0,0 +1,106 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.modelbundle.api; + +import software.amazon.smithy.java.server.ProxyService; +import software.amazon.smithy.java.server.Service; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.ModelAssembler; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.modelbundle.api.model.SmithyBundle; + +public final class ModelBundles { + + private ModelBundles() {} + + private static final PluginProviders PLUGIN_PROVIDERS = PluginProviders.builder().build(); + + public static Service getService(SmithyBundle smithyBundle) { + var model = getModel(smithyBundle); + var plugin = PLUGIN_PROVIDERS.getPlugin(smithyBundle.getConfigType(), smithyBundle.getConfig()); + return ProxyService.builder() + .model(model) + .clientConfigurator(plugin::configureClient) + .service(ShapeId.from(smithyBundle.getServiceName())) + .build(); + } + + private static Model getModel(SmithyBundle bundle) { + var modelAssemble = new ModelAssembler().putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true) + .addUnparsedModel("bundle.json", bundle.getModel()); + var additionalInput = bundle.getAdditionalInput(); + Model model; + StructureShape additionalInputShape = null; + if (additionalInput != null) { + modelAssemble.addUnparsedModel("additionalInput.smithy", additionalInput.getModel()); + model = modelAssemble.assemble().unwrap(); + additionalInputShape = + model.expectShape(ShapeId.from(additionalInput.getIdentifier())).asStructureShape().get(); + + } else { + model = modelAssemble.assemble().unwrap(); + } + var b = model.toBuilder(); + + // mix in the generic arg members + for (var op : model.getOperationShapes()) { + boolean skipOperation = false; + if (op.getOutput().isPresent()) { + for (var member : model.expectShape(op.getOutputShape(), StructureShape.class).members()) { + if (model.expectShape(member.getTarget()).hasTrait(StreamingTrait.class)) { + b.removeShape(op.toShapeId()); + skipOperation = true; + break; + } + } + } + + if (skipOperation) { + continue; + } + + if (op.getInput().isEmpty() && additionalInputShape != null) { + b.addShape(op.toBuilder() + .input(additionalInputShape) + .build()); + } else { + var shape = model.expectShape(op.getInputShape(), StructureShape.class); + for (var member : shape.members()) { + if (model.expectShape(member.getTarget()).hasTrait(StreamingTrait.class)) { + b.removeShape(op.toShapeId()); + skipOperation = true; + break; + } + } + + if (skipOperation) { + continue; + } + + if (additionalInputShape != null) { + var input = shape.toBuilder(); + for (var member : additionalInputShape.members()) { + input.addMember(member.toBuilder() + .id(ShapeId.from(input.getId().toString() + "$" + member.getMemberName())) + .build()); + } + b.addShape(input.build()); + } + } + } + + for (var service : model.getServiceShapes()) { + b.addShape(service.toBuilder() + // trim the endpoint rules because they're huge and we don't need them + .removeTrait(ShapeId.from("smithy.rules#endpointRuleSet")) + .removeTrait(ShapeId.from("smithy.rules#endpointTests")) + .build()); + } + return b.build(); + } +} diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/PluginProviders.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/PluginProviders.java similarity index 95% rename from model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/PluginProviders.java rename to model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/PluginProviders.java index e3909d29f..35337c8a8 100644 --- a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/PluginProviders.java +++ b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/PluginProviders.java @@ -16,7 +16,7 @@ private PluginProviders(Builder builder) { this.providers = builder.providers; } - public BundlePlugin getProvider(String identifier, Document input) { + public BundlePlugin getPlugin(String identifier, Document input) { var provider = providers.get(identifier); if (provider == null) { throw new NullPointerException("no auth provider named " + identifier); diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ServiceLoaderLoader.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ServiceLoaderLoader.java similarity index 100% rename from model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ServiceLoaderLoader.java rename to model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/ServiceLoaderLoader.java diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/StaticAuthSchemeResolver.java b/model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/StaticAuthSchemeResolver.java similarity index 100% rename from model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/StaticAuthSchemeResolver.java rename to model-bundle/model-bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/StaticAuthSchemeResolver.java diff --git a/model-bundler/bundle-api/src/main/resources/META-INF/smithy/bundle.smithy b/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/bundle.smithy similarity index 82% rename from model-bundler/bundle-api/src/main/resources/META-INF/smithy/bundle.smithy rename to model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/bundle.smithy index 96cdc062d..da40288e4 100644 --- a/model-bundler/bundle-api/src/main/resources/META-INF/smithy/bundle.smithy +++ b/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/bundle.smithy @@ -2,7 +2,7 @@ $version: "2" namespace software.amazon.smithy.modelbundle.api -structure Bundle { +structure SmithyBundle { /// unique identifier for the configuration type. used to resolve the appropriate Bundler. @required configType: String @@ -17,21 +17,19 @@ structure Bundle { /// model that describes the service. The service given in `serviceName` must be present. @required - model: Model + model: SmithyModel /// model describing the generic arguments that must be present in every request. If this /// bundle does not require generic arguments, this field may be omitted. - requestArguments: GenericArguments + additionalInput: AdditionalInput } -union Model { - smithyModel: String -} +string SmithyModel -structure GenericArguments { +structure AdditionalInput { @required identifier: String @required - model: Model + model: SmithyModel } diff --git a/model-bundler/bundle-api/src/main/resources/META-INF/smithy/manifest b/model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/manifest similarity index 100% rename from model-bundler/bundle-api/src/main/resources/META-INF/smithy/manifest rename to model-bundle/model-bundle-api/src/main/resources/META-INF/smithy/manifest diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/Bundler.java b/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/Bundler.java deleted file mode 100644 index 597a0d454..000000000 --- a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/Bundler.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.modelbundle.api; - -import software.amazon.smithy.modelbundle.api.model.Bundle; -import software.amazon.smithy.utils.SmithyInternalApi; -import software.amazon.smithy.utils.SmithyUnstableApi; - -@SmithyInternalApi -@SmithyUnstableApi -public interface Bundler { - Bundle bundle(); -} diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlerFactory.java b/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlerFactory.java deleted file mode 100644 index d58e61a2d..000000000 --- a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/BundlerFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.modelbundle.api; - -public interface BundlerFactory { - String identifier(); - - Bundler newBundler(String... args); -} diff --git a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/Bundlers.java b/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/Bundlers.java deleted file mode 100644 index 24ef78426..000000000 --- a/model-bundler/bundle-api/src/main/java/software/amazon/smithy/modelbundle/api/Bundlers.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.modelbundle.api; - -import java.util.HashMap; -import java.util.Map; - -public final class Bundlers { - private final Map providers; - - private Bundlers(Builder builder) { - this.providers = builder.providers; - } - - public Bundler getProvider(String identifier, String... args) { - var provider = providers.get(identifier); - if (provider == null) { - throw new NullPointerException("no bundler named " + identifier); - } - - return provider.newBundler(args); - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private static final Map BASE_PROVIDERS = - ServiceLoaderLoader.load(BundlerFactory.class, BundlerFactory::identifier); - - private Map providers; - - private Builder() { - - } - - public Builder addProvider(BundlerFactory provider) { - if (providers == null) { - providers = new HashMap<>(BASE_PROVIDERS); - } - providers.put(provider.identifier(), provider); - return this; - } - - public Bundlers build() { - if (providers == null) { - providers = BASE_PROVIDERS; - } - return new Bundlers(this); - } - - } -} diff --git a/model-bundler/bundle-cli/build.gradle.kts b/model-bundler/bundle-cli/build.gradle.kts deleted file mode 100644 index d64461907..000000000 --- a/model-bundler/bundle-cli/build.gradle.kts +++ /dev/null @@ -1,55 +0,0 @@ -plugins { - application - id("smithy-java.module-conventions") - alias(libs.plugins.shadow) -} - -description = "This module implements the model-bundler utility" - -extra["displayName"] = "Smithy :: Java :: Model Bundler" -extra["moduleName"] = "software.amazon.smithy.java.modelbundle.cli" - -dependencies { - implementation(project(":core")) - implementation(project(":model-bundler:bundle-api")) - implementation(libs.smithy.model) - implementation(project(":codecs:json-codec")) - - // TODO dynamically load this dependency instead of bundling it - implementation(project(":aws:aws-service-bundler")) - - shadow(project(":core")) - shadow(project(":model-bundler:bundle-api")) -} - -val mainClassName = "software.amazon.smithy.java.modelbundler.ModelBundler" -application { - mainClass.set(mainClassName) - applicationName = "model-bundler" -} - -tasks { - shadowJar { - archiveClassifier.set("") - mergeServiceFiles() - manifest { - attributes["Main-Class"] = mainClassName - } - } - - jar { - finalizedBy(shadowJar) - } - - distZip { - dependsOn(shadowJar) - } - - distTar { - dependsOn(shadowJar) - } - - startScripts { - dependsOn(shadowJar) - } -} diff --git a/model-bundler/bundle-cli/smithy-build.json b/model-bundler/bundle-cli/smithy-build.json deleted file mode 100644 index dc6c899ae..000000000 --- a/model-bundler/bundle-cli/smithy-build.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "1.0", - "plugins": { - "java-type-codegen": { - "namespace": "software.amazon.smithy.model.bundle", - "headerFile": "license.txt" - } - } -} diff --git a/model-bundler/bundle-cli/src/main/java/software/amazon/smithy/java/modelbundle/cli/ModelBundler.java b/model-bundler/bundle-cli/src/main/java/software/amazon/smithy/java/modelbundle/cli/ModelBundler.java deleted file mode 100644 index 661ad9209..000000000 --- a/model-bundler/bundle-cli/src/main/java/software/amazon/smithy/java/modelbundle/cli/ModelBundler.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package software.amazon.smithy.java.modelbundle.cli; - -import java.io.BufferedOutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import software.amazon.smithy.java.json.JsonCodec; -import software.amazon.smithy.modelbundle.api.Bundlers; - -public final class ModelBundler { - public static void main(String[] args) { - // TODO: arg parsing - // remove the output argument from the provided arguments - if (args.length < 2) { - throw new RuntimeException("Expected at least two arguments: a bundler name and an output location"); - } - - var bundlerName = args[0]; - var output = args[1]; - - String[] argsWithoutOutput = new String[args.length - 2]; - if (args.length > 2) { - System.arraycopy(args, 2, argsWithoutOutput, 0, argsWithoutOutput.length); - } - - var bundle = Bundlers.builder().build().getProvider(bundlerName, argsWithoutOutput).bundle(); - var codec = JsonCodec.builder().build(); - try (var os = new BufferedOutputStream(Files.newOutputStream(Path.of(output))); - var serializer = codec.createSerializer(os)) { - bundle.serialize(serializer); - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/server/server-api/src/main/java/software/amazon/smithy/java/server/OperationFilters.java b/server/server-api/src/main/java/software/amazon/smithy/java/server/OperationFilters.java index e1e4b3112..df2ca708f 100644 --- a/server/server-api/src/main/java/software/amazon/smithy/java/server/OperationFilters.java +++ b/server/server-api/src/main/java/software/amazon/smithy/java/server/OperationFilters.java @@ -5,6 +5,7 @@ package software.amazon.smithy.java.server; +import java.util.Collection; import java.util.Collections; import java.util.Set; import java.util.function.Predicate; @@ -22,9 +23,9 @@ private OperationFilters() {} * @return A {@link Predicate} that only includes the specified operations */ public static Predicate> allowList( - Set operationNames + Collection operationNames ) { - return new OperationFilters.OperationNameFilter(operationNames, null); + return new OperationFilters.OperationNameFilter(Set.copyOf(operationNames), null); } /** @@ -33,10 +34,10 @@ private OperationFilters() {} * @param operationNames Set of operation names to exclude * @return A {@link Predicate} that excludes the specified operations */ - static Predicate> blockList( - Set operationNames + public static Predicate> blockList( + Collection operationNames ) { - return new OperationNameFilter(null, operationNames); + return new OperationNameFilter(null, Set.copyOf(operationNames)); } static final class OperationNameFilter implements Predicate> { diff --git a/server/server-proxy/build.gradle.kts b/server/server-proxy/build.gradle.kts index 3fae7783d..6ff103163 100644 --- a/server/server-proxy/build.gradle.kts +++ b/server/server-proxy/build.gradle.kts @@ -13,12 +13,11 @@ dependencies { api(project(":core")) api(project(":context")) api(project(":framework-errors")) - api(project(":model-bundler:bundle-api")) + api(project(":client:dynamic-client")) implementation(libs.smithy.model) implementation(project(":io")) implementation(project(":logging")) implementation(project(":dynamic-schemas")) - implementation(project(":client:dynamic-client")) implementation(project(":client:client-core")) implementation(project(":aws:client:aws-client-http")) } diff --git a/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyService.java b/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyService.java index 5c2a52014..f12af51ae 100644 --- a/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyService.java +++ b/server/server-proxy/src/main/java/software/amazon/smithy/java/server/ProxyService.java @@ -11,10 +11,10 @@ import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; +import java.util.function.UnaryOperator; import software.amazon.smithy.java.auth.api.identity.Identity; import software.amazon.smithy.java.auth.api.identity.IdentityResolver; import software.amazon.smithy.java.aws.client.core.settings.RegionSetting; -import software.amazon.smithy.java.client.core.RequestOverrideConfig; import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver; import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; import software.amazon.smithy.java.core.error.ModeledException; @@ -29,90 +29,41 @@ import software.amazon.smithy.java.dynamicschemas.StructDocument; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; -import software.amazon.smithy.model.loader.ModelAssembler; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.ToShapeId; -import software.amazon.smithy.modelbundle.api.BundlePlugin; -import software.amazon.smithy.modelbundle.api.PluginProviders; -import software.amazon.smithy.modelbundle.api.model.Bundle; import software.amazon.smithy.utils.SmithyUnstableApi; @SmithyUnstableApi public final class ProxyService implements Service { - private static final PluginProviders PLUGIN_PROVIDERS = PluginProviders.builder().build(); private final DynamicClient dynamicClient; private final SchemaConverter schemaConverter; private final Map> operations; private final TypeRegistry serviceErrorRegistry; - private final Model model; private final List> allOperations; private final Schema schema; - private final BundlePlugin plugin; - - private static software.amazon.smithy.model.Model adapt(Builder builder) { - if (builder.bundle == null || builder.bundle.getRequestArguments() == null) { - return builder.model; - } - - var args = builder.bundle.getRequestArguments(); - var model = new ModelAssembler() - .addModel(builder.model) - .addModel(args.getModel().getValue()) - .assemble() - .unwrap(); - var template = model.expectShape(ShapeId.from(args.getIdentifier())) - .asStructureShape() - .get(); - var b = model.toBuilder(); - - // mix in the generic arg members - for (var op : model.getOperationShapes()) { - var input = model.expectShape(op.getInput().get(), StructureShape.class).toBuilder(); - for (var member : template.members()) { - input.addMember(member.toBuilder() - .id(ShapeId.from(input.getId().toString() + "$" + member.getMemberName())) - .build()); - } - b.addShape(input.build()); - } - - for (var service : model.getServiceShapes()) { - b.addShape(service.toBuilder() - // trim the endpoint rules because they're huge and we don't need them - .removeTrait(ShapeId.from("smithy.rules#endpointRuleSet")) - .removeTrait(ShapeId.from("smithy.rules#endpointTests")) - .build()); - } - - return b.build(); - } private ProxyService(Builder builder) { - this.model = adapt(builder); + var model = builder.model; DynamicClient.Builder clientBuilder = DynamicClient.builder() .service(builder.service) - .model(model); - if (builder.bundle != null) { - this.plugin = PLUGIN_PROVIDERS.getProvider(builder.bundle.getConfigType(), builder.bundle.getConfig()); - clientBuilder.endpointResolver(EndpointResolver.staticEndpoint("http://placeholder")); + .model(builder.model); + if (builder.identityResolver != null) { + clientBuilder.addIdentityResolver(builder.identityResolver); + } + if (builder.authScheme != null) { + clientBuilder.authSchemeResolver(builder.authScheme); + } + if (builder.region != null) { + clientBuilder.putConfig(RegionSetting.REGION, builder.region); + } + if (builder.clientConfigurator != null) { + clientBuilder = builder.clientConfigurator.apply(clientBuilder); } else { - this.plugin = null; - // TODO: render this as a bundle clientBuilder.endpointResolver(EndpointResolver.staticEndpoint(builder.proxyEndpoint)); - if (builder.identityResolver != null) { - clientBuilder.addIdentityResolver(builder.identityResolver); - } - if (builder.authScheme != null) { - clientBuilder.authSchemeResolver(builder.authScheme); - } - if (builder.region != null) { - clientBuilder.putConfig(RegionSetting.REGION, builder.region); - } } this.dynamicClient = clientBuilder.build(); this.schemaConverter = new SchemaConverter(model); @@ -120,7 +71,7 @@ private ProxyService(Builder builder) { var registryBuilder = TypeRegistry.builder(); var service = model.expectShape(builder.service, ServiceShape.class); for (var e : service.getErrors()) { - registerError(e, builder.service, registryBuilder); + registerError(e, builder.service, registryBuilder, model); } this.serviceErrorRegistry = registryBuilder.build(); this.allOperations = new ArrayList<>(); @@ -132,8 +83,7 @@ private ProxyService(Builder builder) { schemaConverter, model, operation, - service, - plugin); + service); Operation serverOperation = Operation.of(operationName, function, @@ -142,7 +92,7 @@ private ProxyService(Builder builder) { model, service, serviceErrorRegistry, - (e, rb) -> registerError(e, builder.service, rb)), + (e, rb) -> registerError(e, builder.service, rb, model)), this); allOperations.add(serverOperation); operations.put(operationName, serverOperation); @@ -150,7 +100,7 @@ private ProxyService(Builder builder) { this.schema = schemaConverter.getSchema(service); } - private void registerError(ShapeId e, ShapeId serviceId, TypeRegistry.Builder registryBuilder) { + private void registerError(ShapeId e, ShapeId serviceId, TypeRegistry.Builder registryBuilder, Model model) { var error = model.expectShape(e); var errorSchema = schemaConverter.getSchema(error); registryBuilder.putType(e, @@ -191,7 +141,7 @@ public static final class Builder { private IdentityResolver identityResolver; private String proxyEndpoint; private AuthSchemeResolver authScheme; - private Bundle bundle; + private UnaryOperator clientConfigurator; private Builder() {} @@ -253,16 +203,8 @@ public Builder region(String region) { return this; } - public Builder bundle(Bundle bundle) { - this.bundle = bundle; - this.model = new ModelAssembler() - .putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true) - .addUnparsedModel("bundle.json", bundle.getModel().getValue()) - // synthetic members may cause certain validations to fail, but it's ok for this application - .disableValidation() - .assemble() - .unwrap(); - this.service = ShapeId.from(bundle.getServiceName()); + public Builder clientConfigurator(UnaryOperator clientConfigurator) { + this.clientConfigurator = clientConfigurator; return this; } } @@ -273,17 +215,11 @@ private record DynamicFunction( SchemaConverter schemaConverter, Model model, OperationShape operationShape, - ServiceShape serviceShape, - BundlePlugin plugin) implements BiFunction { + ServiceShape serviceShape) implements BiFunction { @Override public StructDocument apply(StructDocument input, RequestContext requestContext) { - RequestOverrideConfig bundleSettings = null; - if (plugin != null) { - bundleSettings = plugin.buildOverride(input).build(); - } - return createStructDocument(operationShape.getOutput().get(), - dynamicClient.call(operation, input, bundleSettings)); + return createStructDocument(operationShape.getOutput().get(), dynamicClient.call(operation, input)); } private StructDocument createStructDocument(ToShapeId shape, Document value) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 216b63e33..b41b81968 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,11 +70,12 @@ include(":aws:aws-service-bundle") include(":aws:aws-service-bundler") include(":aws:aws-mcp-provider") include(":aws:aws-mcp-types") +include(":aws:aws-mcp-cli-commands") // AWS SDK V2 shims include(":aws:sdkv2:aws-sdkv2-retries") include(":aws:sdkv2:aws-sdkv2-shapes") -include("aws:sdkv2:aws-sdkv2-auth") +include(":aws:sdkv2:aws-sdkv2-auth") // Examples include(":examples") @@ -91,6 +92,9 @@ include(":examples:mcp-server") include(":mcp") include(":mcp:mcp-schemas") include(":mcp:mcp-server") +include(":mcp:mcp-cli") +include(":mcp:mcp-cli-api") +include(":mcp:mcp-bundle-api") -include(":model-bundler:bundle-api") -include(":model-bundler:bundle-cli") +include(":model-bundle") +include(":model-bundle:model-bundle-api") \ No newline at end of file