Skip to content

First cut of the smithy-mcp CLI #677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions aws/aws-mcp-cli-commands/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<String> allowedApis;

@Option(names = {"-b", "--blocked-apis"}, description = "List of APIs to hide in the MCP server")
protected Set<String> 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<String> allowedTools() {
return allowedApis;
}

@Override
protected Set<String> blockedTools() {
return blockedApis;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
software.amazon.smithy.java.aws.mcp.cli.commands.AddAwsServiceBundle
2 changes: 1 addition & 1 deletion aws/aws-service-bundle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <C extends Client, B extends Client.Builder<C, B>> 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());
}
}
}
2 changes: 1 addition & 1 deletion aws/aws-service-bundler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, String> GH_URIS_BY_SERVICE = new HashMap<>();

static {
// line is in the form fooService/service/version/fooService.json
try (var models = new BufferedReader(new InputStreamReader(
Expand All @@ -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()
Expand All @@ -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) {
Expand All @@ -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<String, String> parseEndpoints(ObjectNode endpointTests) {
var testCases = endpointTests.expectArrayMember("testCases");
var endpoints = new HashMap<String, String>();
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions examples/mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
Loading