Skip to content
Open
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
34 changes: 34 additions & 0 deletions mcp/mcp-schemas/model/main.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ structure JsonRpcErrorResponse {
data: Document
}

/// Error data for the -32022 UnsupportedProtocolVersionError (MCP 2026-07-28): names the
/// versions the server supports so the client can pick one and retry.
structure UnsupportedProtocolVersionErrorData {
@required
supported: StringList

@required
requested: String
}

@private
@length(min: 1)
string NonEmptyString
Expand Down Expand Up @@ -83,6 +93,30 @@ structure ServerInfo {
version: String
}

/// Result of the `server/discover` request (MCP 2026-07-28): advertises the protocol versions
/// the server supports, its capabilities, and (in `_meta`) its identity.
structure DiscoverResult {
@required
resultType: String = "complete"

@required
supportedVersions: StringList

@required
capabilities: Capabilities

@required
ttlMs: Long = 0

@required
cacheScope: String = "private"

instructions: String

@jsonName("_meta")
meta: Document
}

structure ListToolsResult {
tools: ToolInfoList
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import software.amazon.smithy.java.mcp.OneOfTrait;
import software.amazon.smithy.java.mcp.model.CallToolResult;
import software.amazon.smithy.java.mcp.model.Capabilities;
import software.amazon.smithy.java.mcp.model.DiscoverResult;
import software.amazon.smithy.java.mcp.model.InitializeResult;
import software.amazon.smithy.java.mcp.model.JsonArraySchema;
import software.amazon.smithy.java.mcp.model.JsonDocumentSchema;
Expand All @@ -61,6 +62,7 @@
import software.amazon.smithy.java.mcp.model.ToolAnnotations;
import software.amazon.smithy.java.mcp.model.ToolInfo;
import software.amazon.smithy.java.mcp.model.Tools;
import software.amazon.smithy.java.mcp.model.UnsupportedProtocolVersionErrorData;
import software.amazon.smithy.java.server.Operation;
import software.amazon.smithy.java.server.Service;
import software.amazon.smithy.model.shapes.ShapeId;
Expand All @@ -78,6 +80,9 @@ public final class McpService {
private static final InternalLogger LOG = InternalLogger.getLogger(McpService.class);
private static final Context.Key<Boolean> ASYNC_DISPATCH = Context.key("mcp.asyncDispatch");
private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601;
private static final int UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022;
private static final String PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion";
private static final String SERVER_INFO_META_KEY = "io.modelcontextprotocol/serverInfo";

private static final JsonCodec CODEC = JsonCodec.builder()
.settings(JsonSettings.builder()
Expand Down Expand Up @@ -133,7 +138,9 @@ public final class McpService {
* <li><b>Asynchronous (callback):</b> For proxy tool calls, returns {@code null} and the callback
* is invoked when the proxy responds.</li>
* <li><b>Neither:</b> For notifications, returns {@code null} and the callback is never
* invoked. Requests with unknown methods receive a -32601 (Method not found) error.</li>
* invoked. Requests with unknown methods receive a -32601 (Method not found) error, and
* requests naming an unsupported protocol version in {@code params._meta} receive a
* -32022 (Unsupported protocol version) error.</li>
* </ul>
*
* @param req The JSON-RPC request to handle
Expand Down Expand Up @@ -162,21 +169,25 @@ public JsonRpcResponse handleRequest(
// Dispatch
validate(currentReq);
var method = currentReq.getMethod();
response = switch (method) {
case "initialize" -> handleInitialize(currentReq);
case "ping" -> handlePing(currentReq);
default -> {
initializeProxies(rpcResponse -> {});
yield switch (method) {
case "prompts/list" -> handlePromptsList(currentReq);
case "prompts/get" -> handlePromptsGet(currentReq);
case "tools/list" -> handleToolsList(currentReq, protocolVersion);
case "tools/call" ->
handleToolsCall(currentReq, asyncResponseCallback, protocolVersion, hook);
default -> methodNotFound(currentReq);
};
}
};
response = unsupportedProtocolVersion(method, currentReq);
if (response == null) {
response = switch (method) {
case "initialize" -> handleInitialize(currentReq);
case "ping" -> handlePing(currentReq);
case "server/discover" -> handleServerDiscover(currentReq);
default -> {
initializeProxies(rpcResponse -> {});
yield switch (method) {
case "prompts/list" -> handlePromptsList(currentReq);
case "prompts/get" -> handlePromptsGet(currentReq);
case "tools/list" -> handleToolsList(currentReq, protocolVersion);
case "tools/call" ->
handleToolsCall(currentReq, asyncResponseCallback, protocolVersion, hook);
default -> methodNotFound(currentReq);
};
}
};
}
if (Boolean.TRUE.equals(hook.context().get(ASYNC_DISPATCH))) {
return null;
}
Expand All @@ -201,9 +212,14 @@ private JsonRpcResponse handleRequestDirect(
try {
validate(req);
var method = req.getMethod();
var versionError = unsupportedProtocolVersion(method, req);
if (versionError != null) {
return versionError;
}
return switch (method) {
case "initialize" -> handleInitialize(req);
case "ping" -> handlePing(req);
case "server/discover" -> handleServerDiscover(req);
default -> {
initializeProxies(rpcResponse -> {});
yield switch (method) {
Expand Down Expand Up @@ -296,7 +312,11 @@ private JsonRpcResponse handleInitialize(JsonRpcRequest req) {
String pv = null;
if (maybeVersion != null) {
var protocolVersion = ProtocolVersion.version(maybeVersion.asString());
if (!(protocolVersion instanceof ProtocolVersion.UnknownVersion)) {
if (protocolVersion instanceof ProtocolVersion.UnknownVersion) {
// Per the MCP spec, a server that does not support the requested protocol
// version must respond with the latest version it supports.
pv = ProtocolVersion.latestVersion().identifier();
} else {
pv = protocolVersion.identifier();
}
}
Expand All @@ -307,14 +327,8 @@ private JsonRpcResponse handleInitialize(JsonRpcRequest req) {
}

var result = builder
.capabilities(Capabilities.builder()
.tools(Tools.builder().listChanged(true).build())
.prompts(Prompts.builder().listChanged(true).build())
.build())
.serverInfo(ServerInfo.builder()
.name(serviceName)
.version(version)
.build())
.capabilities(serverCapabilities())
.serverInfo(serverInfo())
.build();

return createSuccessResponse(req.getId(), result);
Expand All @@ -328,6 +342,83 @@ private JsonRpcResponse handlePing(JsonRpcRequest req) {
.build();
}

/**
* Handles {@code server/discover} (MCP 2026-07-28), which servers must implement to advertise
* their supported protocol versions, capabilities, and identity. As the negotiation bootstrap
* it is answered regardless of the protocol version the request carries. Until the modern
* (2026-07-28) era is fully supported, the advertised versions are all handshake-era —
* an explicit legacy advertisement that dual-era clients answer by falling back to the
* {@code initialize} handshake.
*/
private JsonRpcResponse handleServerDiscover(JsonRpcRequest req) {
var result = DiscoverResult.builder()
.supportedVersions(ProtocolVersion.supportedIdentifiers())
.capabilities(serverCapabilities())
.meta(Document.of(Map.of(SERVER_INFO_META_KEY, Document.of(serverInfo()))))
.build();
return createSuccessResponse(req.getId(), result);
}

private static Capabilities serverCapabilities() {
return Capabilities.builder()
.tools(Tools.builder().listChanged(true).build())
.prompts(Prompts.builder().listChanged(true).build())
.build();
}

private ServerInfo serverInfo() {
return ServerInfo.builder()
.name(serviceName)
.version(version)
.build();
}

/**
* Per MCP 2026-07-28, a request that names a protocol version this server does not implement
* (via the {@code io.modelcontextprotocol/protocolVersion} key of {@code params._meta}) must
* receive an UnsupportedProtocolVersionError (-32022) listing the versions the server does
* support. Returns {@code null} when the request may proceed: the method is a negotiation
* bootstrap ({@code initialize}, {@code server/discover}), no version was requested, the
* version is supported, or the message is a notification (JSON-RPC forbids responding).
*/
private static JsonRpcResponse unsupportedProtocolVersion(String method, JsonRpcRequest req) {
if ("initialize".equals(method) || "server/discover".equals(method)) {
return null;
}
var params = req.getParams();
if (params == null) {
return null;
}
var meta = params.getMember("_meta");
if (meta == null) {
return null;
}
var requestedMember = meta.getMember(PROTOCOL_VERSION_META_KEY);
if (requestedMember == null || !requestedMember.isType(ShapeType.STRING)) {
return null;
}
var requested = requestedMember.asString();
if (!(ProtocolVersion.version(requested) instanceof ProtocolVersion.UnknownVersion)) {
return null;
}
if (req.getId() == null) {
return null;
}
var error = JsonRpcErrorResponse.builder()
.code(UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE)
.message("Unsupported protocol version")
.data(Document.of(UnsupportedProtocolVersionErrorData.builder()
.supported(ProtocolVersion.supportedIdentifiers())
.requested(requested)
.build()))
.build();
return JsonRpcResponse.builder()
.id(req.getId())
.error(error)
.jsonrpc("2.0")
.build();
}

private JsonRpcResponse handlePromptsList(JsonRpcRequest req) {
var result = ListPromptsResult.builder()
.prompts(prompts.values().stream().map(Prompt::promptInfo).toList())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

package software.amazon.smithy.java.mcp.server;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import software.amazon.smithy.utils.SmithyUnstableApi;

@SmithyUnstableApi
Expand Down Expand Up @@ -49,6 +52,24 @@ private UnknownVersion(String identifier) {
}
}

/**
* Holder defers initialization until first use so the version subclasses are fully loaded
* first — a plain static field on this class would read a still-null INSTANCE whenever a
* subclass is the first member of the hierarchy to be initialized.
*/
private static final class SupportedVersions {
private static final List<ProtocolVersion> ALL = List.of(
v2024_11_05.INSTANCE,
v2025_03_26.INSTANCE,
v2025_06_18.INSTANCE,
v2025_11_25.INSTANCE);
private static final ProtocolVersion LATEST = Collections.max(ALL);
private static final List<String> IDENTIFIERS_NEWEST_FIRST = ALL.stream()
.sorted(Comparator.reverseOrder())
.map(ProtocolVersion::identifier)
.toList();
}

private final String identifier;

private ProtocolVersion(String identifier) {
Expand All @@ -72,17 +93,35 @@ public final int compareTo(ProtocolVersion o) {
}

public static ProtocolVersion version(String identifier) {
return switch (identifier) {
case null -> v2025_03_26.INSTANCE;
case "2024-11-05" -> v2024_11_05.INSTANCE;
case "2025-03-26" -> v2025_03_26.INSTANCE;
case "2025-06-18" -> v2025_06_18.INSTANCE;
case "2025-11-25" -> v2025_11_25.INSTANCE;
default -> new UnknownVersion(identifier);
};
if (identifier == null) {
return defaultVersion();
}
for (var version : SupportedVersions.ALL) {
if (version.identifier.equals(identifier)) {
return version;
}
}
return new UnknownVersion(identifier);
}

public static ProtocolVersion defaultVersion() {
return v2025_03_26.INSTANCE;
}

/**
* The most recent protocol version this server supports, derived from the supported-version
* registry.
*/
public static ProtocolVersion latestVersion() {
return SupportedVersions.LATEST;
}

/**
* Identifiers of every protocol version this server supports, newest first — the order
* clients expect in {@code server/discover} results and in the {@code supported} list of
* UnsupportedProtocolVersionError data.
*/
public static List<String> supportedIdentifiers() {
return SupportedVersions.IDENTIFIERS_NEWEST_FIRST;
}
}
Loading