Skip to content

Conversation

@monrax
Copy link
Contributor

@monrax monrax commented Jan 13, 2026

This PR:

  • Implements spec-compliant version negotiation: silent fallback during initialization (i.e. 200 OK response with server-supported protocol version) and strict MCP-Protocol-Version header validation for subsequent requests (i.e. 400 Bad Request responses for invalid/unsupported protocol versions).
  • Removes custom McpException in favor of McpError in SDK.

This ensures proper error handling according to the Model Context Protocol specification and fixes compatibility issues with MCP clients like MCP Inspector and fastmcp.

Description

Spec Compliance

Per 2025-06-18:

If the server supports the requested protocol version, it MUST respond with the same version. Otherwise, the server MUST respond with another protocol version it supports. This SHOULD be the latest version supported by the server.

If the server receives a request with an invalid or unsupported MCP-Protocol-Version, it MUST respond with 400 Bad Request.
For backwards compatibility, if the server does not receive an MCP-Protocol-Version header, and has no other way to identify the version [...] the server SHOULD assume protocol version 2025-03-26.

File changes

McpException.java

Replaced with standard McpError from MCP SDK

McpRestResource.java

  • Protocol Version Handling:

    • Validate MCP-Protocol-Version header on all requests (when present).
    • Return 400 Bad Request for invalid/unsupported header values per spec.
    • Default to "2025-03-26" when header is missing per spec backwards compatibility requirement.
    • Added TODO for future session management to retrieve negotiated version from session state.
  • Error Handling:

    • Catch McpError specifically and return 400 Bad Request with proper JSON-RPC error structure.
    • Extract request ID early (before any errors) to ensure all error responses include the correct ID.
    • Improved invalid/unsupported error messages with structured data (i.e., supported vs requested versions).

McpService.java

  • Version Negotiation:

    • Changed supportedVersions to protected static final for proper access from REST resource.
    • Added FALLBACK_MCP_VERSION constant ("2025-03-26") per spec requirement.
    • Implements silent fallback during initialization: if client requests unsupported version, server responds with its latest supported version instead of throwing error.
  • Error Handling:

    • Changed method signatures from throws McpException to throws McpError.
    • Standardized error construction using McpError.builder().
    • Consistent error codes: METHOD_NOT_FOUND, RESOURCE_NOT_FOUND, INVALID_PARAMS.
    • Include structured error data (e.g., URI, supported versions) for better debugging.
  • API Changes:

    • Added overloaded handle() method accepting protocolVersion parameter for future multi-version support.
    • Added TODO indicating where version-specific handlers could be implemented.

McpServiceTest.java

  • Changed testInitializeWithUnsupportedVersion() to expect successful fallback instead of exception.
  • Verify response includes server's supported version when client requests unsupported version.
  • Updated error assertions from McpException to McpError.
  • Updated expected error messages to match new standardized messages.

Motivation and Context

Fixes #24701

The previous implementation had two main issues:

  1. Incorrect version negotiation: During initialization, the server was rejecting clients that requested unsupported protocol versions with a 400 Bad Request error. Per the MCP specification, servers must silently fall back to a supported version and return 200 OK, allowing the client to decide whether to continue.
  2. Custom exception handling: Used a custom McpException class instead of the standard McpError types provided by the MCP SDK, leading to inconsistent error codes and messages.

How Has This Been Tested?

  • All existing tests pass with updated expectations.
  • Tested with MCP Inspector and fastmcp/claude, with both now successfully complete initialization.
  • Version negotiation works correctly: clients requesting unsupported versions receive server's supported version and can proceed:
curl -v -X POST -H "Content-type:application/json" -H "X-Sent-By:me" http://admin:admin@localhost:9000/api/mcp -d '
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": {
        "listChanged": false
      }
    }
  }
}
'
> POST /api/mcp HTTP/1.1
> Host: localhost:9000
> Authorization: Basic YWRtaW46YWRtaW4=
> User-Agent: curl/8.7.1
> Accept: */*
> Content-type:application/json
> X-Sent-By:me
> Content-Length: 199
> 
* upload completely sent off: 199 bytes
< HTTP/1.1 200 OK
< Mcp-Session-Id: d2e85481-c9ec-477a-a43c-84704a2ab2ad
< X-Frame-Options: DENY
< X-Content-Type-Options: nosniff
< X-Graylog-Node-ID: 10b4a276-d245-49de-a581-f21e391b2ed4
< Content-Type: application/json
< Content-Length: 256
< 
* Connection #0 to host localhost left intact
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-06-18","capabilities":{"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"Graylog","version":"7.1.0-SNAPSHOT"}}}

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (non-breaking change)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have requested a documentation update.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

- Implements spec-compliant version negotiation:  silent fallback (i.e. 200 OK with server-supported protocol version) during initialization and strict MCP-Protocol-Version header validation for subsequent requests (i.e. 400 Bad Request for invalid protocol versions).
- Removes custom McpException in favor of McpError in SDK.
Copy link
Member

@kroepke kroepke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall 👍 but I've left some minor comments for clarity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove the unused/default fields here, they'd end up in the changelog otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. Thanks!

@Singleton
public class McpService {
private static final Logger LOG = LoggerFactory.getLogger(McpService.class);
protected static final List<String> supportedVersions = List.of(ProtocolVersions.MCP_2025_06_18);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use getFirst below, and I believe the intention is that the list is sorted in descending order, returning the latest version we support.
If we really need a List here, we should note that requirement in a comment or simply support a single version. I guess just adding a comment is fine.

We typically capitalize constant fields, and annotat/comment it if the relaxed visibility is for testing (iirc @VisibleForTesting annotation)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I just added LATEST_SUPPORTED_MCP_VERSION to avoid the sorting issue altogether. The list is there so we can add versions as we support them (we should probably support at least "2025-03-26" too since it's supposed to be the fallback version but that's prob for another time).

I don't think we need the annotation here, as the added visibility is for the rest resource. It could still be reduced tho, so i went ahead and modified that as well.

Thank you!

final McpSchema.InitializeResult result = new McpSchema.InitializeResult(
initializeRequest.protocolVersion(),
// Version negotiation: https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#version-negotiation
supportedVersions.contains(initializeRequest.protocolVersion()) ? initializeRequest.protocolVersion() : supportedVersions.getFirst(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supportedVersions.getFirst() relies on statically ordered supportedVersions above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Member

@kroepke kroepke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx!

@monrax monrax merged commit 45d97f0 into master Jan 19, 2026
22 checks passed
@monrax monrax deleted the fix/mcp-protocol-negotiation branch January 19, 2026 16:21
garybot2 pushed a commit that referenced this pull request Jan 19, 2026
* fix(mcp): protocol version negotiation

- Implements spec-compliant version negotiation:  silent fallback (i.e. 200 OK with server-supported protocol version) during initialization and strict MCP-Protocol-Version header validation for subsequent requests (i.e. 400 Bad Request for invalid protocol versions).
- Removes custom McpException in favor of McpError in SDK.

(cherry picked from commit 45d97f0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: protocol version negotiation

3 participants