Skip to content

Commit e099948

Browse files
committed
Add MCP Support
1 parent 4a52c25 commit e099948

25 files changed

Lines changed: 1244 additions & 4 deletions

File tree

client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/DocumentException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ public Document getContents() {
5151
return document;
5252
}
5353

54-
static final class SchemaGuidedExceptionBuilder implements ShapeBuilder<ModeledException> {
54+
public static final class SchemaGuidedExceptionBuilder implements ShapeBuilder<ModeledException> {
5555

5656
private final Schema target;
5757
private final ShapeBuilder<WrappedDocument> delegateBuilder;
5858

59-
SchemaGuidedExceptionBuilder(ShapeId service, Schema target) {
59+
public SchemaGuidedExceptionBuilder(ShapeId service, Schema target) {
6060
this.target = target;
6161
this.delegateBuilder = SchemaConverter.createDocumentBuilder(target, service);
6262
}

client/dynamic-client/src/main/java/software/amazon/smithy/java/dynamicclient/DynamicOperation.java

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55

66
package software.amazon.smithy.java.dynamicclient;
77

8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.HashSet;
811
import java.util.List;
912
import java.util.Set;
13+
import java.util.function.BiConsumer;
1014
import software.amazon.smithy.java.core.schema.ApiOperation;
1115
import software.amazon.smithy.java.core.schema.ApiService;
1216
import software.amazon.smithy.java.core.schema.Schema;
@@ -15,9 +19,13 @@
1519
import software.amazon.smithy.java.core.serde.TypeRegistry;
1620
import software.amazon.smithy.java.dynamicschemas.SchemaConverter;
1721
import software.amazon.smithy.java.dynamicschemas.WrappedDocument;
22+
import software.amazon.smithy.model.Model;
23+
import software.amazon.smithy.model.knowledge.ServiceIndex;
24+
import software.amazon.smithy.model.shapes.OperationShape;
25+
import software.amazon.smithy.model.shapes.ServiceShape;
1826
import software.amazon.smithy.model.shapes.ShapeId;
1927

20-
final class DynamicOperation implements ApiOperation<WrappedDocument, WrappedDocument> {
28+
public final class DynamicOperation implements ApiOperation<WrappedDocument, WrappedDocument> {
2129

2230
private final ApiService service;
2331
private final Schema operationSchema;
@@ -27,7 +35,7 @@ final class DynamicOperation implements ApiOperation<WrappedDocument, WrappedDoc
2735
private final TypeRegistry typeRegistry;
2836
private final List<ShapeId> effectiveAuthSchemes;
2937

30-
public DynamicOperation(
38+
DynamicOperation(
3139
ApiService service,
3240
Schema operationSchema,
3341
Schema inputSchema,
@@ -94,4 +102,54 @@ public TypeRegistry errorRegistry() {
94102
public List<ShapeId> effectiveAuthSchemes() {
95103
return effectiveAuthSchemes;
96104
}
105+
106+
public static DynamicOperation create(
107+
OperationShape shape,
108+
SchemaConverter schemaConverter,
109+
Model model,
110+
ServiceShape service,
111+
TypeRegistry serviceErrorRegistry,
112+
BiConsumer<ShapeId, TypeRegistry.Builder> registerErrorCallback
113+
) {
114+
var operationSchema = schemaConverter.getSchema(shape);
115+
116+
List<ShapeId> authSchemes = new ArrayList<>();
117+
for (var trait : ServiceIndex.of(model).getEffectiveAuthSchemes(service).values()) {
118+
authSchemes.add(trait.toShapeId());
119+
}
120+
121+
var inputSchema = schemaConverter.getSchema(model.expectShape(shape.getInputShape()));
122+
var outputSchema = schemaConverter.getSchema(model.expectShape(shape.getOutputShape()));
123+
124+
// Default to using the service registry.
125+
var registry = serviceErrorRegistry;
126+
127+
var errorSchemas = new HashSet<Schema>();
128+
// Create a type registry that is able to deserialize errors using schemas.
129+
if (!shape.getErrors().isEmpty()) {
130+
var registryBuilder = TypeRegistry.builder();
131+
for (var e : shape.getErrors()) {
132+
registerErrorCallback.accept(e, registryBuilder);
133+
errorSchemas.add(schemaConverter.getSchema(model.expectShape(e)));
134+
}
135+
// Compose the operation errors with the service errors.
136+
registry = TypeRegistry.compose(registryBuilder.build(), serviceErrorRegistry);
137+
}
138+
139+
var serviceSchema = schemaConverter.getSchema(service);
140+
var apiService = new ApiService() {
141+
@Override
142+
public Schema schema() {
143+
return serviceSchema;
144+
}
145+
};
146+
return new DynamicOperation(
147+
apiService,
148+
operationSchema,
149+
inputSchema,
150+
outputSchema,
151+
Collections.unmodifiableSet(errorSchemas),
152+
registry,
153+
authSchemes);
154+
}
97155
}

codecs/json-codec/src/main/java/software/amazon/smithy/java/json/JsonSettings.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public final class JsonSettings {
4343
private final boolean forbidUnknownUnionMembers;
4444
private final String defaultNamespace;
4545
private final JsonSerdeProvider provider;
46+
private final boolean serializeTypeInDocuments;
4647

4748
private JsonSettings(Builder builder) {
4849
this.timestampResolver = builder.useTimestampFormat
@@ -54,6 +55,7 @@ private JsonSettings(Builder builder) {
5455
this.forbidUnknownUnionMembers = builder.forbidUnknownUnionMembers;
5556
this.defaultNamespace = builder.defaultNamespace;
5657
this.provider = builder.provider;
58+
this.serializeTypeInDocuments = builder.serializeTypeInDocuments;
5759
}
5860

5961
/**
@@ -139,6 +141,7 @@ public static final class Builder {
139141
private boolean forbidUnknownUnionMembers;
140142
private String defaultNamespace;
141143
private JsonSerdeProvider provider = PROVIDER;
144+
private boolean serializeTypeInDocuments = true;
142145

143146
private Builder() {}
144147

@@ -216,6 +219,16 @@ public Builder defaultNamespace(String defaultNamespace) {
216219
return this;
217220
}
218221

222+
/**
223+
*
224+
* @param serializeTypeInDocuments
225+
* @return
226+
*/
227+
public Builder serializeTypeInDocuments(boolean serializeTypeInDocuments) {
228+
this.serializeTypeInDocuments = serializeTypeInDocuments;
229+
return this;
230+
}
231+
219232
/**
220233
* Uses a custom JSON serde provider.
221234
*

core/src/main/java/software/amazon/smithy/java/core/schema/TraitKey.java

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

88
import java.util.concurrent.atomic.AtomicInteger;
99
import software.amazon.smithy.model.traits.DefaultTrait;
10+
import software.amazon.smithy.model.traits.DocumentationTrait;
1011
import software.amazon.smithy.model.traits.EndpointTrait;
1112
import software.amazon.smithy.model.traits.EnumTrait;
1213
import software.amazon.smithy.model.traits.ErrorTrait;
@@ -62,6 +63,7 @@ protected TraitKey<?> computeValue(Class<?> clazz) {
6263
// Note that TraitKeys can be accessed at any time through TraitKey#get; these are just pre-defined.
6364

6465
public static final TraitKey<RequiredTrait> REQUIRED_TRAIT = TraitKey.get(RequiredTrait.class);
66+
public static final TraitKey<DocumentationTrait> DOCUMENTATION_TRAIT = TraitKey.get(DocumentationTrait.class);
6567
public static final TraitKey<DefaultTrait> DEFAULT_TRAIT = TraitKey.get(DefaultTrait.class);
6668
public static final TraitKey<UniqueItemsTrait> UNIQUE_ITEMS_TRAIT = TraitKey.get(UniqueItemsTrait.class);
6769
public static final TraitKey<TimestampFormatTrait> TIMESTAMP_FORMAT_TRAIT = TraitKey.get(

examples/mcp-server/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
## Example: MCP Server
2+
3+
### Usage
4+
5+
To use this example as a template, run the following command with
6+
the [Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/index.html):
7+
8+
```console
9+
smithy init -t mcp-server --url https://github.com/smithy-lang/smithy-java
10+
```
11+
12+
Or
13+
14+
```console
15+
smithy init -t mcp-server --url git@github.com:smithy-lang/smithy-java.git
16+
```
17+
18+
To generate a fat jar which contains all the dependencies required to run
19+
a [Model Context Protocol](https://modelcontextprotocol.io/) (
20+
MCP) [StdIO](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio) server,
21+
run the following from the root of the project:
22+
23+
```console
24+
gradle build
25+
```
26+
27+
This will generate a fat JAR file at `build/libs/mcp-server-0.0.1-all.jar`. This artifact includes all the necessary
28+
code to create an MCP server that uses the StdIO transport.
29+
30+
There are two example implementations included:
31+
32+
* `MCPServerExample` : Demonstrates how to build an MCP server by modeling tools as Smithy APIs.
33+
34+
* `ProxyMCPExample` : Shows how to create a Proxy MCP Server for any Smithy service. In this example, a Smithy Java
35+
server is started on port 8080, and the MCP server proxies requests to it.
36+
37+
You can run the Proxy MCP Server using the following command:
38+
39+
```
40+
java -cp mcp-server-0.0.1-all.jar software.amazon.smithy.java.example.server.mcp.ProxyMCPExample
41+
```
42+
43+
To run the direct MCP server example instead, simply replace `ProxyMCPExample` with `MCPServerExample`.
44+
45+
Here's how you might configure the MCP client to invoke the proxy server:
46+
47+
```json
48+
{
49+
"mcpServers": {
50+
"smithy-mcp-server": {
51+
"command": "java",
52+
"args": [
53+
"-cp",
54+
"/path/to/build/libs/mcp-server-0.0.1-all.jar",
55+
"software.amazon.smithy.java.example.server.mcp.ProxyMCPExample"
56+
]
57+
}
58+
}
59+
}
60+
```
61+
62+
63+
64+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer
2+
3+
plugins {
4+
`java-library`
5+
id("software.amazon.smithy.gradle.smithy-base")
6+
id("com.gradleup.shadow").version("8.3.5")
7+
}
8+
9+
dependencies {
10+
val smithyJavaVersion: String by project
11+
12+
smithyBuild("software.amazon.smithy.java:plugins:$smithyJavaVersion")
13+
implementation("software.amazon.smithy.java:server-proxy:$smithyJavaVersion")
14+
implementation("software.amazon.smithy.java:server-mcp:$smithyJavaVersion")
15+
implementation("software.amazon.smithy.java:server-netty:$smithyJavaVersion")
16+
implementation("software.amazon.smithy.java:aws-server-restjson:$smithyJavaVersion")
17+
implementation("software.amazon.smithy.java:aws-client-restjson:$smithyJavaVersion")
18+
}
19+
20+
// Add generated Java files to the main sourceSet
21+
afterEvaluate {
22+
val serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen")
23+
sourceSets {
24+
main {
25+
java {
26+
srcDir(serverPath)
27+
}
28+
}
29+
}
30+
}
31+
32+
tasks {
33+
compileJava {
34+
dependsOn(smithyBuild)
35+
}
36+
}
37+
38+
repositories {
39+
mavenLocal()
40+
mavenCentral()
41+
}
42+
43+
tasks.shadowJar {
44+
val shadePrefix = "software.amazon.smithy.java.internal"
45+
relocate("com.jsoniter", "$shadePrefix.jsoniter")
46+
relocate("com.fasterxml.jackson", "$shadePrefix.com.fasterxml.jackson")
47+
relocate("META-INF/native/libnetty", "META-INF/native/lib${shadePrefix.replace('.', '_')}_netty")
48+
relocate("META-INF/native/netty", "META-INF/native/${shadePrefix.replace('.', '_')}_netty")
49+
exclude("META-INF/maven/**")
50+
mergeServiceFiles()
51+
transform(AppendingTransformer::class.java) {
52+
resource = "META-INF/smithy/manifest"
53+
}
54+
}
55+
56+
tasks.assemble {
57+
dependsOn(tasks.shadowJar)
58+
}
59+

examples/mcp-server/license.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/*
2+
* Example file license header.
3+
* File header line two
4+
*/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Basic usage of generated server stubs.
3+
*/
4+
5+
pluginManagement {
6+
val smithyGradleVersion: String by settings
7+
8+
plugins {
9+
id("software.amazon.smithy.gradle.smithy-base").version(smithyGradleVersion)
10+
id("com.gradleup.shadow").version("8.3.5")
11+
}
12+
13+
repositories {
14+
mavenLocal()
15+
mavenCentral()
16+
gradlePluginPortal()
17+
}
18+
}
19+
20+
rootProject.name = "SmithyJavaMCPServer"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": "1.0",
3+
"sources" : ["src/main/resources/software/amazon/smithy/java/example/server/mcp"],
4+
"plugins": {
5+
"java-server-codegen": {
6+
"service": "smithy.example.mcp#EmployeeService",
7+
"namespace": "software.amazon.smithy.java.example.server.mcp",
8+
"headerFile": "license.txt",
9+
"runtimeTraits": ["smithy.api#documentation", "smithy.api#examples" ]
10+
}
11+
}
12+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package software.amazon.smithy.java.example.server.mcp;
2+
3+
import software.amazon.smithy.java.example.server.mcp.operations.GetCodingStatistics;
4+
import software.amazon.smithy.java.example.server.mcp.operations.GetEmployeeDetails;
5+
import software.amazon.smithy.java.example.server.mcp.service.EmployeeService;
6+
import software.amazon.smithy.java.server.mcp.MCPServer;
7+
8+
public class MCPServerExample {
9+
10+
public static void main(String[] args) {
11+
var service = EmployeeService.builder()
12+
.addGetCodingStatisticsOperation(new GetCodingStatistics())
13+
.addGetEmployeeDetailsOperation(new GetEmployeeDetails())
14+
.build();
15+
16+
var mcpServer = MCPServer.builder()
17+
.stdio()
18+
.name("smithy-mcp-server")
19+
.addService(service)
20+
.build();
21+
22+
mcpServer.start();
23+
24+
try {
25+
Thread.currentThread().join();
26+
} catch (InterruptedException e) {
27+
mcpServer.shutdown();
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)