Skip to content
Draft
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
57 changes: 34 additions & 23 deletions examples/mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## Example: MCP Server

This example contains two newline-delimited JSON-RPC servers using the MCP
standard input/output transport:

- `MCPServerExample` exposes generated Smithy service implementations directly.
- `ProxyMCPExample` starts a Smithy HTTP server on port `8080` and exposes a
`ProxyService` for it through MCP.

### Usage

To use this example as a template, run the following command with
Expand All @@ -15,34 +22,42 @@ Or
smithy init -t mcp-server --url [email protected]:smithy-lang/smithy-java.git
```

To generate a fat jar which contains all the dependencies required to run
a [Model Context Protocol](https://modelcontextprotocol.io/) (
MCP) [StdIO](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio) server,
run the following from the root of the project:
The generated server uses the transport-specific `StdioMcpServer` entry point:

```console
gradle build
```
```java
var mcpServer = StdioMcpServer.builder()
.stdio()
.name("smithy-mcp-server")
.addService("employee-mcp", service)
.build();

This will generate a fat JAR file at `build/libs/mcp-server-0.0.1-all.jar`. This artifact includes all the necessary
code to create an MCP server that uses the StdIO transport.
mcpServer.start();
mcpServer.awaitCompletion();
```

There are two example implementations included:
To compile both implementations and generate a fat JAR from a Smithy Java
checkout, run:

* `MCPServerExample` : Demonstrates how to build an MCP server by modeling tools as Smithy APIs.
```console
./gradlew :examples:mcp-server:build
```

* `ProxyMCPExample` : Shows how to create a Proxy MCP Server for any Smithy service. In this example, a Smithy Java
server is started on port 8080, and the MCP server proxies requests to it.
The fat JAR is written to
`examples/mcp-server/build/libs/mcp-server-<version>-all.jar`. It contains the
generated service code, both example entry points, and the MCP standard
input/output transport.

You can run the Proxy MCP Server using the following command:
Run the proxy example from the repository root with:

```
java -cp mcp-server-0.0.1-all.jar software.amazon.smithy.java.example.server.mcp.ProxyMCPExample
```console
java -cp examples/mcp-server/build/libs/mcp-server-*-all.jar \
software.amazon.smithy.java.example.server.mcp.ProxyMCPExample
```

To run the direct MCP server example instead, simply replace `ProxyMCPExample` with `MCPServerExample`.
Replace `ProxyMCPExample` with `MCPServerExample` to run the direct service
implementation.

Here's how you might configure the MCP client to invoke the proxy server:
An MCP client can launch the proxy server with a configuration like:

```json
{
Expand All @@ -51,14 +66,10 @@ Here's how you might configure the MCP client to invoke the proxy server:
"command": "java",
"args": [
"-cp",
"/path/to/build/libs/mcp-server-0.0.1-all.jar",
"/path/to/smithy-java/examples/mcp-server/build/libs/mcp-server-<version>-all.jar",
"software.amazon.smithy.java.example.server.mcp.ProxyMCPExample"
]
}
}
}
```




6 changes: 5 additions & 1 deletion examples/mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,9 @@ tasks.assemble {
}

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(21))
toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}

tasks.withType<JavaCompile>() {
options.release.set(25)
}
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.mcp.server.McpServer;
import software.amazon.smithy.java.mcp.server.StdioMcpServer;

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 = StdioMcpServer.builder()
.stdio()
.name("smithy-mcp-server")
.addService("employee-mcp", service)
Expand All @@ -22,8 +22,10 @@ public static void main(String[] args) {
mcpServer.start();

try {
Thread.currentThread().join();
mcpServer.awaitCompletion();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
mcpServer.shutdown();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,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.mcp.server.McpServer;
import software.amazon.smithy.java.mcp.server.StdioMcpServer;
import software.amazon.smithy.java.server.ProxyService;
import software.amazon.smithy.java.server.Server;
import software.amazon.smithy.model.Model;
Expand Down Expand Up @@ -38,16 +38,18 @@ public static void main(String[] args) {
.proxyEndpoint("http://localhost:8080")
.build();

var mcpServer = McpServer.builder()
var mcpServer = StdioMcpServer.builder()
.stdio()
.name("smithy-mcp-server")
.addService("employee-mcp", mcpService)
.build();
mcpServer.start();

try {
Thread.currentThread().join();
mcpServer.awaitCompletion();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
mcpServer.shutdown();
server.shutdown();
}
Expand Down
5 changes: 5 additions & 0 deletions mcp/mcp-schemas/model/main.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ structure InitializeResult with [BaseResult] {
}

structure Capabilities {
completions: Document
logging: Document
prompts: Prompts
tools: Tools
Expand Down Expand Up @@ -152,6 +153,10 @@ structure JsonPrimitiveSchema {

/// JSON Schema format annotation (e.g., "date-time" for timestamps)
format: String

/// MCP HTTP parameter header suffix from smithy.ai#mcpHeader.
@jsonName("x-mcp-header")
mcpHeader: String
}

structure JsonDocumentSchema {
Expand Down
140 changes: 140 additions & 0 deletions mcp/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,143 @@
> This module is not recommended for production use.

Provides Model Context Protocol (MCP) server support for Smithy Java, enabling MCP server generation from Smithy models.

## Creating a standard input/output server

Generated Smithy services can be exposed directly:

```java
var mcpServer = StdioMcpServer.builder()
.stdio()
.name("employee-server")
.version("1.0.0")
.addService("employees", employeeService)
.build();

mcpServer.start();
mcpServer.awaitCompletion();
```

For applications that need to share execution across transports, construct the
transport-independent engine separately:

```java
var engine = McpEngine.builder()
.name("employee-server")
.addService("employees", employeeService)
.build();

var stdioServer = StdioMcpServer.builder()
.stdio()
.engine(engine)
.build();
```

Builder-managed services and a prebuilt engine are mutually exclusive.

## Architecture and extension points

The implementation is split into a blocking, transport-independent `McpEngine`,
typed sealed `McpCall` and `McpOutcome` hierarchies, declarative per-version
protocol profiles, an immutable-snapshot source aggregator, and transport adapters:

- `StdioMcpServer` exposes an engine over newline-delimited JSON-RPC and executes
requests on virtual threads.
- `McpHttpHandler` adapts decoded Streamable HTTP requests.
- `HttpMcpClient` and `StdioMcpClient` are blocking remote clients intended to
run naturally on virtual threads.
- `McpExtensionMethod` adds typed custom methods without modifying the built-in
protocol dispatch.
- `ExtensionMcpProtocol` is the open branch of the sealed `McpProtocol`
hierarchy for externally implemented protocol versions.

Unsupported operations default to JSON-RPC method-not-found responses. A new
built-in protocol revision is added as one immutable method/feature declaration,
and the exhaustive version switch makes an incomplete registration fail at compile
time.

## Adding a protocol

Implement `ExtensionMcpProtocol` and override only the behavior that differs from
the defaults:

```java
public final class FutureProtocol implements ExtensionMcpProtocol {
private static final McpProtocolId ID = McpProtocolId.of("2099-01-01");

@Override
public McpProtocolId id() {
return ID;
}

@Override
public Set<McpMethod.Standard> supportedMethods() {
return Set.of(
McpMethod.Standard.INITIALIZE,
McpMethod.Standard.PING,
McpMethod.Standard.TOOLS_LIST,
McpMethod.Standard.TOOLS_CALL);
}

@Override
public McpProtocolFeatures features() {
return new McpProtocolFeatures(true, true, false, false, false);
}
}
```

Register it directly:

```java
var engine = McpEngine.builder()
.addProtocol(new FutureProtocol())
.build();
```

Or publish it through Java's service-provider mechanism:

```java
public final class FutureProtocolProvider implements McpProtocolProvider {
@Override
public Collection<? extends ExtensionMcpProtocol> protocols() {
return List.of(new FutureProtocol());
}
}
```

Register the provider class in:

```text
META-INF/services/software.amazon.smithy.java.mcp.server.McpProtocolProvider
```

Built-in protocols, discovered providers, and builder registrations share one
immutable registry. Duplicate identifiers fail engine construction. This ensures
that upgrading to a release that implements a previously external protocol does
not silently change behavior. Use `overrideProtocol` only when replacement is
intentional:

```java
var engine = McpEngine.builder()
.overrideProtocol(new FutureProtocol())
.build();
```

`discoverProtocols(false)` disables service-provider discovery. Programmatically
registered protocols remain enabled.

Use `McpInterceptor` to observe or replace immutable calls and outcomes. Custom
method implementations use `McpExtensionMethod<P>` and are registered with
`McpEngine.Builder.addExtension`. Its outbound `encode` operation defaults to
`UnsupportedOperationException`, so inbound-only extensions implement only
decoding and execution.

The module supports protocol revisions through `2026-07-28`. Run the official
Model Context Protocol conformance scenarios with:

```console
./gradlew :mcp:mcp-server:conformance
```

The conformance task requires Node.js and invokes the pinned
`@modelcontextprotocol/conformance` package.
18 changes: 18 additions & 0 deletions mcp/mcp-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("smithy-java.module-conventions")
id("smithy-java.codegen-plugin-conventions")
`java-test-fixtures`
}

description =
Expand Down Expand Up @@ -32,3 +33,20 @@ spotbugs {
}

addGenerateSrcsTask("software.amazon.smithy.java.mcp.server.utils.TestJavaCodegenRunner", null, null, "server")

tasks.named<Test>("integ") {
useJUnitPlatform {
excludeTags("conformance")
}
}

tasks.register<Test>("conformance") {
description = "Runs the official Model Context Protocol conformance scenarios"
group = "verification"
useJUnitPlatform {
includeTags("conformance")
}
testClassesDirs = sourceSets["it"].output.classesDirs
classpath = sourceSets["it"].runtimeClasspath
shouldRunAfter("integ")
}
Loading
Loading