Skip to content

Commit df4999a

Browse files
committed
Add create command and have a concept of local bundles
1 parent 43830f4 commit df4999a

File tree

16 files changed

+442
-57
lines changed

16 files changed

+442
-57
lines changed

aws/aws-mcp-cli-commands/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ dependencies {
2424
tasks.compileJava {
2525
options.compilerArgs.add("-Aproject=${project.name}")
2626
}
27+
28+
tasks.withType<JavaCompile> {
29+
options.release.set(21)
30+
}

aws/aws-mcp-cli-commands/src/main/java/software/amazon/smithy/java/aws/mcp/cli/commands/AddAwsServiceBundle.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import software.amazon.smithy.mcp.bundle.api.model.BundleMetadata;
1818
import software.amazon.smithy.mcp.bundle.api.model.SmithyMcpBundle;
1919

20-
@Command(name = "add-aws-bundle")
20+
@Command(name = "add-aws-bundle", hidden = true, description = "Deprecated!. Use the create command.")
2121
public class AddAwsServiceBundle extends AbstractAddBundle {
2222

2323
@Option(names = "--overwrite",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.aws.mcp.cli.commands;
7+
8+
import static picocli.CommandLine.ArgGroup;
9+
import static picocli.CommandLine.Parameters;
10+
11+
import java.util.Set;
12+
import picocli.CommandLine.Command;
13+
import picocli.CommandLine.Option;
14+
import software.amazon.smithy.java.aws.servicebundle.bundler.AwsServiceBundler;
15+
import software.amazon.smithy.java.mcp.cli.AbstractCreateBundle;
16+
import software.amazon.smithy.mcp.bundle.api.model.Bundle;
17+
import software.amazon.smithy.mcp.bundle.api.model.BundleMetadata;
18+
import software.amazon.smithy.mcp.bundle.api.model.SmithyMcpBundle;
19+
20+
@Command(name = "aws-service-mcp", description = "Create an MCP for an AWS service.")
21+
public class CreateAwsServiceBundle extends AbstractCreateBundle<CreateAwsServiceBundle.CreateAwsServiceBundleInput> {
22+
23+
@ArgGroup(multiplicity = "1")
24+
Input input;
25+
26+
@Override
27+
protected CreateAwsServiceBundleInput getInput() {
28+
return input.cliInput;
29+
}
30+
31+
@Override
32+
protected Bundle getNewBundle(CreateAwsServiceBundleInput input) {
33+
var bundleBuilder = AwsServiceBundler.builder()
34+
.serviceName(input.awsServiceName);
35+
if (input.allowedApis != null) {
36+
// User explicitly specified allowed APIs
37+
bundleBuilder.exposedOperations(input.allowedApis);
38+
// If readOnlyApis is also requested, include those as well
39+
if (Boolean.TRUE.equals(input.readOnlyApis)) {
40+
bundleBuilder.readOnlyOperations();
41+
}
42+
} else if (!Boolean.FALSE.equals(input.readOnlyApis)) {
43+
//If nothing is specified then default to only readOnlyOperations.
44+
bundleBuilder.readOnlyOperations();
45+
} else {
46+
throw new IllegalArgumentException("You have turned off readOnlyApis and also not specified any " +
47+
"allowedApis so there are no operations to create a bundle for.");
48+
}
49+
50+
if (input.blockedApis != null) {
51+
// Always apply blocked operations if specified
52+
bundleBuilder.blockedOperations(input.blockedApis);
53+
}
54+
return Bundle.builder()
55+
.smithyBundle(SmithyMcpBundle.builder()
56+
.bundle(bundleBuilder.build().bundle())
57+
.metadata(BundleMetadata.builder()
58+
.name(input.name)
59+
.description(input.description)
60+
.build())
61+
.build())
62+
.build();
63+
}
64+
65+
public static class Input {
66+
@ArgGroup(exclusive = false)
67+
CreateAwsServiceBundleInput cliInput;
68+
69+
@ArgGroup(multiplicity = "1")
70+
JsonInput jsonInput;
71+
72+
}
73+
74+
public static class CreateAwsServiceBundleInput extends CreateBundleInput {
75+
@Parameters(description = "Name of aws service to create the bundle for.")
76+
String awsServiceName;
77+
78+
@Option(names = {"-a", "--allowed-apis"}, description = "List of APIs to expose in the MCP server")
79+
protected Set<String> allowedApis;
80+
81+
@Option(names = {"-b", "--blocked-apis"}, description = "List of APIs to hide in the MCP server")
82+
protected Set<String> blockedApis;
83+
84+
@Option(names = "--read-only-apis",
85+
description = "Include read only APIs in the MCP server")
86+
protected Boolean readOnlyApis;
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
software.amazon.smithy.java.aws.mcp.cli.commands.CreateAwsServiceBundle

mcp/mcp-bundle-api/src/main/java/software/amazon/smithy/mcp/bundle/api/ModifiableRegistry.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77

88
import software.amazon.smithy.mcp.bundle.api.model.Bundle;
99

10-
public interface ModifiableRegistry extends Registry {
11-
void publishBundle(Bundle bundle);
10+
public interface ModifiableRegistry<T> extends Registry {
11+
void publishBundle(Bundle bundle, T args);
1212
}

mcp/mcp-cli-api/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ tasks.named("compileJava") {
5151
dependsOn("smithyBuild")
5252
}
5353

54+
tasks.withType<JavaCompile> {
55+
options.release.set(21)
56+
}
57+
5458
// Needed because sources-jar needs to run after smithy-build is done
5559
tasks.sourcesJar {
5660
mustRunAfter(tasks.compileJava)

mcp/mcp-cli-api/model/main.smithy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ structure CommonToolConfig {
3333

3434
@required
3535
bundleLocation: Location
36+
37+
@default(false)
38+
local: PrimitiveBoolean
39+
40+
description: String
3641
}
3742

3843
map Registries {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.mcp.cli;
7+
8+
import static picocli.CommandLine.Option;
9+
10+
import picocli.CommandLine.ArgGroup;
11+
import software.amazon.smithy.mcp.bundle.api.model.Bundle;
12+
13+
public abstract class AbstractCreateBundle<T extends AbstractCreateBundle.CreateBundleInput> extends SmithyMcpCommand {
14+
15+
public abstract static class CreateBundleInput {
16+
@Option(names = "--overwrite",
17+
description = "Overwrite existing MCP server.",
18+
defaultValue = "false")
19+
boolean overwrite;
20+
21+
@Option(names = {"-n", "--name"}, description = "Name to assign to the MCP server. Eg: (aws-dynamodb-mcp)",
22+
required = true)
23+
public String name;
24+
25+
@Option(names = {"-d", "--description"}, description = "Description of this mcp server", required = true)
26+
public String description;
27+
28+
@ArgGroup
29+
ClientsInput clientsInput;
30+
}
31+
32+
public static class JsonInput {
33+
@Option(names = {"--input"}, description = "Provide the input in json format.", required = true)
34+
String inputJson;
35+
36+
@Option(names = {"--input-file"}, description = "Provide path to a file which contains input in json format.",
37+
required = true)
38+
String inputJsonFile;
39+
}
40+
41+
@Override
42+
protected void execute(ExecutionContext context) throws Exception {
43+
var input = getInput();
44+
var config = context.config();
45+
if (!input.overwrite && config.getToolBundles().containsKey(input.name)) {
46+
throw new IllegalArgumentException("Tool bundle " + input.name
47+
+ " already exists. Either choose a new name or pass --overwrite to overwrite the existing tool bundle");
48+
}
49+
50+
var bundle = getNewBundle(input);
51+
ConfigUtils.addMcpBundle(config, input.name, bundle, true);
52+
ConfigUtils.createWrapperAndUpdateClientConfigs(input.name, bundle, config, input.clientsInput);
53+
System.out.println("Successfully created bundle " + input.name);
54+
}
55+
56+
protected abstract T getInput();
57+
58+
protected abstract Bundle getNewBundle(T input);
59+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.mcp.cli;
7+
8+
import java.util.Set;
9+
import picocli.CommandLine.Option;
10+
11+
public final class ClientsInput {
12+
@Option(names = {"--clients"},
13+
description = "Names of client configs to update. If not specified all client configs registered would be updated")
14+
public Set<String> clients = Set.of();
15+
16+
@Option(names = "--print-client-config",
17+
description = "If specified will not edit the client configs and only print to console.")
18+
public Boolean print;
19+
}

mcp/mcp-cli-api/src/main/java/software/amazon/smithy/java/mcp/cli/ConfigUtils.java

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import software.amazon.smithy.mcp.bundle.api.model.Bundle;
4242
import software.amazon.smithy.mcp.bundle.api.model.ExecSpec;
4343
import software.amazon.smithy.mcp.bundle.api.model.GenericBundle;
44+
import software.amazon.smithy.mcp.bundle.api.model.SmithyMcpBundle;
4445

4546
/**
4647
* Utility class for managing Smithy MCP configuration files.
@@ -282,38 +283,48 @@ private static Set<ClientConfig> getClientConfigsToUpdate(Config config, Set<Str
282283
return clientConfigsToUpdate;
283284
}
284285

285-
private static void addMcpBundle(Config config, String toolBundleName, CliBundle mcpBundleConfig)
286+
private static void writeMcpBundle(String toolBundleName, Bundle bundle)
286287
throws IOException {
287-
var serializedBundle = toJson(mcpBundleConfig.mcpBundle());
288+
var serializedBundle = toJson(bundle);
288289
Files.write(getBundleFileLocation(toolBundleName),
289290
serializedBundle,
290291
StandardOpenOption.TRUNCATE_EXISTING,
291292
StandardOpenOption.CREATE);
292-
addMcpBundleConfig(config, toolBundleName, mcpBundleConfig.mcpBundleConfig());
293293
}
294294

295295
public static McpBundleConfig addMcpBundle(Config config, String toolBundleName, Bundle bundle)
296296
throws IOException {
297+
return addMcpBundle(config, toolBundleName, bundle, false);
298+
}
299+
300+
public static McpBundleConfig addMcpBundle(Config config, String toolBundleName, Bundle bundle, boolean isLocal)
301+
throws IOException {
297302
var location = Location.builder()
298303
.fileLocation(ConfigUtils.getBundleFileLocation(toolBundleName).toString())
299304
.build();
300305
var builder = McpBundleConfig.builder();
301-
switch (bundle.type()) {
302-
case smithyBundle -> builder.smithyModeled(SmithyModeledBundleConfig.builder()
306+
switch (bundle.getValue()) {
307+
case SmithyMcpBundle smithyBundle -> builder.smithyModeled(SmithyModeledBundleConfig.builder()
303308
.name(toolBundleName)
309+
.description(smithyBundle.getMetadata().getDescription())
304310
.bundleLocation(location)
311+
.local(isLocal)
305312
.build());
306-
case genericBundle -> {
307-
GenericBundle genericBundle = bundle.getValue();
313+
case GenericBundle genericBundle -> {
308314
install(genericBundle.getInstall());
309315
builder.genericConfig(
310-
GenericToolBundleConfig.builder().name(toolBundleName).bundleLocation(location).build());
316+
GenericToolBundleConfig.builder()
317+
.name(toolBundleName)
318+
.local(isLocal)
319+
.bundleLocation(location)
320+
.description(genericBundle.getMetadata().getDescription())
321+
.build());
311322
}
312323
default -> throw new IllegalStateException("Unexpected bundle type: " + bundle.type());
313324
}
314325

315326
var mcpBundleConfig = builder.build();
316-
addMcpBundle(config, toolBundleName, new CliBundle(bundle, mcpBundleConfig));
327+
writeMcpBundle(toolBundleName, bundle);
317328
addMcpBundleConfig(config, toolBundleName, mcpBundleConfig);
318329
return mcpBundleConfig;
319330
}
@@ -489,4 +500,48 @@ private static void printUnixPathInstructions(String shimsDirStr) {
489500
System.out.println("Add this line to your shell profile (~/.bashrc, ~/.zshrc, etc.) to make it permanent");
490501
System.out.println();
491502
}
503+
504+
public static void createWrapperAndUpdateClientConfigs(
505+
String name,
506+
Bundle bundle,
507+
Config config,
508+
ClientsInput input
509+
) throws IOException {
510+
boolean shouldCreateWrapper = true;
511+
List<String> args = List.of();
512+
String command = name;
513+
if (bundle.getValue() instanceof GenericBundle genericBundle && genericBundle.isExecuteDirectly()) {
514+
command = genericBundle.getRun().getExecutable();
515+
args = genericBundle.getRun().getArgs();
516+
shouldCreateWrapper = false;
517+
}
518+
519+
if (shouldCreateWrapper) {
520+
createWrapperScript(name);
521+
ensureMcpServersDirInPath();
522+
}
523+
524+
var newClientConfig = McpServerConfig.builder()
525+
.command(command)
526+
.args(args)
527+
.build();
528+
//By default, print the output if there are no configured client configs.
529+
Set<String> clientConfigs;
530+
boolean print;
531+
if (input == null) {
532+
clientConfigs = config.getClientConfigs().stream().map(ClientConfig::getName).collect(Collectors.toSet());
533+
print = clientConfigs.isEmpty();
534+
} else {
535+
print = input.print;
536+
clientConfigs = input.clients;
537+
}
538+
539+
if (print) {
540+
System.out.println("You can add the following to your MCP Servers config to use " + name);
541+
var serializedConfig = ByteBufferUtils.getBytes(JSON_CODEC.serialize(newClientConfig));
542+
System.out.println(new String(serializedConfig, StandardCharsets.UTF_8));
543+
} else {
544+
addToClientConfigs(config, name, clientConfigs, newClientConfig);
545+
}
546+
}
492547
}

0 commit comments

Comments
 (0)