Skip to content

Add server/discover and stateless lifecycle error codes per SEP-2575#438

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:server_discover
Open

Add server/discover and stateless lifecycle error codes per SEP-2575#438
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:server_discover

Conversation

@koic

@koic koic commented Jul 3, 2026

Copy link
Copy Markdown
Member

Motivation and Context

SEP-2575 (modelcontextprotocol/modelcontextprotocol#2575, merged for the 2026-07-28 spec release) makes MCP stateless, removing the initialize/notifications/initialized handshake in favor of per-request _meta-carried negotiation. The full lifecycle rewrite is unmerged in both the TypeScript SDK (the closed #2063/#2064/#2251 prototype stack, on hold since 2026-06-08) and the Python SDK, but two pieces of its wire contract stayed stable across every prototype iteration and are purely additive:

  • The server/discover method, which servers MUST implement: sessionless capability discovery returning supportedVersions, capabilities, serverInfo, and instructions. The new Server#discover handler reuses the same data init already builds, but is state-free and idempotent: it stores no client info, never marks a session initialized, and responds regardless of declared capabilities or initialization state (added to the no-capability group in Methods.ensure_capability!). serverInfo is returned unfiltered because discovery precedes version negotiation; the draft's ttlMs/cacheScope fields on DiscoverResult are left to the SEP-2549 work. On StreamableHTTPTransport, a server/discover POST is exempt from the Mcp-Session-Id requirement and the MCP-Protocol-Version header check, like initialize, so clients can probe a server with a bare POST in both stateful and stateless modes.
  • The error code vocabulary: the new MCP::ErrorCodes module exports MISSING_REQUIRED_CLIENT_CAPABILITY = -32003 and UNSUPPORTED_PROTOCOL_VERSION = -32004. Constants only; nothing raises them yet (that belongs to the 2026-07-28 lifecycle work), and the existing inline -32042 of URLElicitationRequiredError is intentionally left untouched.

The removal of initialize, ping, and logging/setLevel that SEP-2575 also specifies is deliberately NOT implemented: the SDK targets protocol versions up to 2025-11-25, where those methods are required.

Part of #389.

How Has This Been Tested?

  • test/mcp/server_test.rb: server/discover returns the supported versions, the server's capabilities, server info, and instructions; it responds before initialize and with empty capabilities; it does not mark the session initialized (a subsequent initialize still succeeds); instructions is omitted when the server has none; and define_custom_method(method_name: "server/discover") now raises MethodAlreadyDefinedError.
  • test/mcp/methods_test.rb: ensure_capability! does not raise for SERVER_DISCOVER with empty capabilities.
  • test/mcp/server/transports/streamable_http_transport_test.rb: a server/discover POST without a session ID returns 200 with the discover result in both stateful and stateless modes, without issuing an Mcp-Session-Id.
  • New test/mcp/error_codes_test.rb pins the -32003/-32004 values.

bundle exec rake (tests, RuboCop, and conformance baseline) passes.

Breaking Changes

None at the wire level for existing clients. One narrow API consequence: servers that previously registered their own "server/discover" handler via define_custom_method now get MethodAlreadyDefinedError because the method is built in.

Types of changes

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

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

SEP-2575 (modelcontextprotocol/modelcontextprotocol#2575, merged for the 2026-07-28 spec release)
makes MCP stateless, removing the `initialize`/`notifications/initialized` handshake in favor of
per-request `_meta`-carried negotiation. The full lifecycle rewrite is unmerged in both
the TypeScript SDK (the closed #2063/#2064/#2251 prototype stack, on hold since 2026-06-08) and
the Python SDK, but two pieces of its wire contract stayed stable across every prototype iteration
and are purely additive:

- The `server/discover` method, which servers MUST implement: sessionless capability discovery
  returning `supportedVersions`, `capabilities`, `serverInfo`, and `instructions`.
  The new `Server#discover` handler reuses the same data `init` already builds, but is state-free
  and idempotent: it stores no client info, never marks a session initialized, and responds regardless
  of declared capabilities or initialization state (added to the no-capability group in `Methods.ensure_capability!`).
  `serverInfo` is returned unfiltered because discovery precedes version negotiation;
  the draft's `ttlMs`/`cacheScope` fields on `DiscoverResult` are left to the SEP-2549 work.
  On `StreamableHTTPTransport`, a `server/discover` POST is exempt from the `Mcp-Session-Id` requirement
  and the `MCP-Protocol-Version` header check, like `initialize`, so clients can probe a server with
  a bare POST in both stateful and stateless modes.
- The error code vocabulary: the new `MCP::ErrorCodes` module exports `MISSING_REQUIRED_CLIENT_CAPABILITY = -32003`
  and `UNSUPPORTED_PROTOCOL_VERSION = -32004`. Constants only; nothing raises them yet
  (that belongs to the 2026-07-28 lifecycle work), and the existing inline `-32042` of
  `URLElicitationRequiredError` is intentionally left untouched.

The removal of `initialize`, `ping`, and `logging/setLevel` that SEP-2575 also specifies is
deliberately NOT implemented: the SDK targets protocol versions up to 2025-11-25,
where those methods are required.

Part of modelcontextprotocol#389.

## How Has This Been Tested?

- `test/mcp/server_test.rb`: `server/discover` returns the supported versions, the server's capabilities,
  server info, and instructions; it responds before `initialize` and with empty capabilities;
  it does not mark the session initialized (a subsequent `initialize` still succeeds);
  `instructions` is omitted when the server has none; and `define_custom_method(method_name: "server/discover")`
  now raises `MethodAlreadyDefinedError`.
- `test/mcp/methods_test.rb`: `ensure_capability!` does not raise for `SERVER_DISCOVER` with empty capabilities.
- `test/mcp/server/transports/streamable_http_transport_test.rb`: a `server/discover` POST without a session ID
  returns 200 with the discover result in both stateful and stateless modes, without issuing an `Mcp-Session-Id`.
- New `test/mcp/error_codes_test.rb` pins the -32003/-32004 values.

`bundle exec rake` (tests, RuboCop, and conformance baseline) passes.

## Breaking Changes

None at the wire level for existing clients. One narrow API consequence: servers that previously registered
their own `"server/discover"` handler via `define_custom_method` now get `MethodAlreadyDefinedError` because
the method is built in.
@koic koic force-pushed the server_discover branch from acb2f9f to c145876 Compare July 4, 2026 11:29
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.

1 participant