mcp: update return meta in DiscoverResult (SEP-2575) (#1097)

This PR implements the latest changes introduced by
https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002
This commit is contained in:
Guglielmo Colombo 2026-07-16 16:10:12 +02:00 • committed by GitHub
commit 5cb8339210
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 210 additions and 68 deletions

View file

@ -1,7 +1,21 @@
server: [] # All tests pass! server:
# The `server-stateless` scenario ships with the `@modelcontextprotocol/conformance@0.2.0-alpha.9`
# suite pinned by the workflow, which still enforces the pre-PR-3002
# (https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002) spec:
# - `clientInfo` in the per-request `_meta` is required (MUST).
# - `serverInfo` is a body field on `DiscoverResult`.
# PR #3002 relaxes `clientInfo` to SHOULD and moves `serverInfo` into every
# response's `_meta`. The Go SDK follows the updated spec, so the following
# checks in the `server-stateless` scenario fail against the pinned suite:
# - sep-2575-request-meta-invalid-missing-client-info
# - sep-2575-http-server-meta-invalid-400
# - sep-2575-server-implements-discover
# Remove this entry when the conformance suite is bumped to a release that
# incorporates PR #3002.
- server-stateless
client: client:
# OAuth mechanisms not yet implemented by the Go MCP SDK. # OAuth mechanisms not implemented by the Go MCP SDK.
- auth/client-credentials-basic - auth/client-credentials-basic
- auth/client-credentials-jwt - auth/client-credentials-jwt
- auth/enterprise-managed-authorization - auth/enterprise-managed-authorization
@ -10,4 +24,6 @@ client:
# server/discover handler advertises "2026-07-28" but delegates every # server/discover handler advertises "2026-07-28" but delegates every
# other method to a pinned @modelcontextprotocol/sdk that rejects that # other method to a pinned @modelcontextprotocol/sdk that rejects that
# version at the HTTP layer. # version at the HTTP layer.
# Remove this entry when the conformance suite is bumped to a release that
# fixes this issue.
- json-schema-ref-no-deref - json-schema-ref-no-deref

View file

@ -4,6 +4,7 @@
1. [Lifecycle](#lifecycle) 1. [Lifecycle](#lifecycle)
1. [Discovery (`server/discover`)](#discovery-(server/discover)) 1. [Discovery (`server/discover`)](#discovery-(server/discover))
1. [Per-request `_meta` keys](#per-request-meta-keys) 1. [Per-request `_meta` keys](#per-request-meta-keys)
1. [Per-response `_meta` keys](#per-response-meta-keys)
1. [Subscriptions (`subscriptions/listen`)](#subscriptions-(subscriptions/listen)) 1. [Subscriptions (`subscriptions/listen`)](#subscriptions-(subscriptions/listen))
1. [Transports](#transports) 1. [Transports](#transports)
1. [Stdio Transport](#stdio-transport) 1. [Stdio Transport](#stdio-transport)
@ -40,8 +41,10 @@ transparently based on the negotiated protocol version:
- A **stateless** model introduced in `2026-07-28` by - A **stateless** model introduced in `2026-07-28` by
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575), [SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575),
in which there is no `initialize`/`notifications/initialized` handshake, and in which there is no `initialize`/`notifications/initialized` handshake, and
each request carries its protocol version, client identity, and client each request carries its protocol version and client capabilities in
capabilities in `_meta`. `_meta`. Clients SHOULD also include their identity (`clientInfo`) on every
request, and servers SHOULD include their identity (`serverInfo`) on every
response.
In both models, the SDK exposes the same API: In both models, the SDK exposes the same API:
@ -130,17 +133,28 @@ When the negotiated protocol version is `2026-07-28` or later, every request
carries these keys inside its `_meta` map (constants live in carries these keys inside its `_meta` map (constants live in
[`mcp/protocol.go`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#pkg-constants)): [`mcp/protocol.go`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#pkg-constants)):
| Constant | Wire key | Type | | Constant | Wire key | Type | Required |
|---|---|---| |---|---|---|---|
| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` | | `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` | Yes |
| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` | | `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` | Yes |
| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` | | `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` | No |
| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) | | `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) | No |
The client populates these keys automatically on every outgoing request. The client populates the required keys automatically on every outgoing
Server-side handlers can read them request, and populates `clientInfo` when configured with an `*Implementation`
through `ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, (the default). Server-side handlers can read them through
and `ServerRequest[P].ClientCapabilities()`. `ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, and
`ServerRequest[P].ClientCapabilities()`.
### Per-response `_meta` keys
Under the same protocol version, servers SHOULD identify themselves on every
response. The SDK populates this
key automatically on every outgoing response:
| Constant | Wire key | Type | Required |
|---|---|---|---|
| `MetaKeyServerInfo` | `io.modelcontextprotocol/serverInfo` | `*Implementation` | No |
### Subscriptions (`subscriptions/listen`) ### Subscriptions (`subscriptions/listen`)

View file

@ -50,7 +50,7 @@ func ExampleLoggingTransport() {
} }
// Output: // Output:
// read: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"}}} // read: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"server","version":"v0.0.1"}},"ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}}}}
// write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}} // write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}
} }
``` ```

View file

@ -17,8 +17,10 @@ transparently based on the negotiated protocol version:
- A **stateless** model introduced in `2026-07-28` by - A **stateless** model introduced in `2026-07-28` by
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575), [SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575),
in which there is no `initialize`/`notifications/initialized` handshake, and in which there is no `initialize`/`notifications/initialized` handshake, and
each request carries its protocol version, client identity, and client each request carries its protocol version and client capabilities in
capabilities in `_meta`. `_meta`. Clients SHOULD also include their identity (`clientInfo`) on every
request, and servers SHOULD include their identity (`serverInfo`) on every
response.
In both models, the SDK exposes the same API: In both models, the SDK exposes the same API:
@ -76,17 +78,28 @@ When the negotiated protocol version is `2026-07-28` or later, every request
carries these keys inside its `_meta` map (constants live in carries these keys inside its `_meta` map (constants live in
[`mcp/protocol.go`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#pkg-constants)): [`mcp/protocol.go`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#pkg-constants)):
| Constant | Wire key | Type | | Constant | Wire key | Type | Required |
|---|---|---| |---|---|---|---|
| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` | | `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` | Yes |
| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` | | `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` | Yes |
| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` | | `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` | No |
| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) | | `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) | No |
The client populates these keys automatically on every outgoing request. The client populates the required keys automatically on every outgoing
Server-side handlers can read them request, and populates `clientInfo` when configured with an `*Implementation`
through `ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, (the default). Server-side handlers can read them through
and `ServerRequest[P].ClientCapabilities()`. `ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, and
`ServerRequest[P].ClientCapabilities()`.
### Per-response `_meta` keys
Under the same protocol version, servers SHOULD identify themselves on every
response. The SDK populates this
key automatically on every outgoing response:
| Constant | Wire key | Type | Required |
|---|---|---|---|
| `MetaKeyServerInfo` | `io.modelcontextprotocol/serverInfo` | `*Implementation` | No |
### Subscriptions (`subscriptions/listen`) ### Subscriptions (`subscriptions/listen`)

View file

@ -443,11 +443,15 @@ func (c *Client) discover(ctx context.Context, cs *ClientSession) (*InitializeRe
} }
} }
var serverInfo *Implementation
if v, ok := decodeMetaValue[*Implementation](res.GetMeta(), MetaKeyServerInfo); ok {
serverInfo = v
}
return &InitializeResult{ return &InitializeResult{
Capabilities: res.Capabilities, Capabilities: res.Capabilities,
Instructions: res.Instructions, Instructions: res.Instructions,
ProtocolVersion: negotiated, ProtocolVersion: negotiated,
ServerInfo: res.ServerInfo, ServerInfo: serverInfo,
}, nil }, nil
} }
@ -509,9 +513,12 @@ func (cs *ClientSession) usesNewProtocol() bool {
return res != nil && res.ProtocolVersion >= protocolVersion20260728 return res != nil && res.ProtocolVersion >= protocolVersion20260728
} }
// injectRequestMeta populates the SEP-2575 per-request `_meta` triple // injectRequestMeta populates the SEP-2575 per-request `_meta` fields
// (protocolVersion, clientInfo, clientCapabilities) on the given outgoing // (protocolVersion, optional clientInfo, clientCapabilities) on the given
// request params. Keys already present in params.Meta are not overwritten. // outgoing request params. Keys already present in params.Meta are not
// overwritten. Per PR modelcontextprotocol/modelcontextprotocol#3002
// clientInfo is SHOULD (not MUST), and is omitted when the client has no
// [Implementation] configured.
func injectRequestMeta[T any, P interface { func injectRequestMeta[T any, P interface {
*T *T
Params Params
@ -527,7 +534,7 @@ func injectRequestMeta[T any, P interface {
if _, ok := m[MetaKeyProtocolVersion]; !ok { if _, ok := m[MetaKeyProtocolVersion]; !ok {
m[MetaKeyProtocolVersion] = res.ProtocolVersion m[MetaKeyProtocolVersion] = res.ProtocolVersion
} }
if _, ok := m[MetaKeyClientInfo]; !ok { if _, ok := m[MetaKeyClientInfo]; !ok && cs.client.impl != nil {
m[MetaKeyClientInfo] = cs.client.impl m[MetaKeyClientInfo] = cs.client.impl
} }
if _, ok := m[MetaKeyClientCapabilities]; !ok { if _, ok := m[MetaKeyClientCapabilities]; !ok {

View file

@ -662,11 +662,13 @@ func TestClientConnectDiscover(t *testing.T) {
name: "discover success skips initialize", name: "discover success skips initialize",
discoverHandler: func() (Result, error) { discoverHandler: func() (Result, error) {
return &DiscoverResult{ return &DiscoverResult{
Meta: Meta{
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
},
SupportedVersions: []string{protocolVersion20260728}, SupportedVersions: []string{protocolVersion20260728},
Capabilities: &ServerCapabilities{ Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: true}, Tools: &ToolCapabilities{ListChanged: true},
}, },
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
}, nil }, nil
}, },
wantInitialize: false, wantInitialize: false,
@ -695,9 +697,11 @@ func TestClientConnectDiscover(t *testing.T) {
name: "no overlapping supported version falls back to initialize", name: "no overlapping supported version falls back to initialize",
discoverHandler: func() (Result, error) { discoverHandler: func() (Result, error) {
return &DiscoverResult{ return &DiscoverResult{
Meta: Meta{
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
},
SupportedVersions: []string{otherVersionsOnly}, SupportedVersions: []string{otherVersionsOnly},
Capabilities: &ServerCapabilities{}, Capabilities: &ServerCapabilities{},
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
}, nil }, nil
}, },
wantInitialize: true, wantInitialize: true,
@ -885,9 +889,11 @@ func TestInMemory_E2E_DiscoverFallback_NoOverlap(t *testing.T) {
return func(ctx context.Context, method string, req Request) (Result, error) { return func(ctx context.Context, method string, req Request) (Result, error) {
if method == methodDiscover { if method == methodDiscover {
return &DiscoverResult{ return &DiscoverResult{
Meta: Meta{
MetaKeyServerInfo: &Implementation{Name: "vpre-like-server", Version: "v1"},
},
SupportedVersions: []string{protocolVersion20251125}, SupportedVersions: []string{protocolVersion20251125},
Capabilities: &ServerCapabilities{}, Capabilities: &ServerCapabilities{},
ServerInfo: &Implementation{Name: "vpre-like-server", Version: "v1"},
}, nil }, nil
} }
return next(ctx, method, req) return next(ctx, method, req)

View file

@ -224,4 +224,12 @@ func TestContentUnmarshalNilWithInvalidContent(t *testing.T) {
} }
} }
var ctrCmpOpts = []cmp.Option{cmpopts.IgnoreUnexported(mcp.CallToolResult{}, mcp.GetPromptResult{}, mcp.ReadResourceResult{})} var ctrCmpOpts = []cmp.Option{
cmpopts.IgnoreUnexported(mcp.CallToolResult{}, mcp.GetPromptResult{}, mcp.ReadResourceResult{}),
// Server responses under the >= 2026-07-28 protocol carry an auto-populated
// [mcp.MetaKeyServerInfo] entry; tests that compare result bodies against
// hand-crafted expected values should ignore it.
cmpopts.IgnoreFields(mcp.CallToolResult{}, "Meta"),
cmpopts.IgnoreFields(mcp.GetPromptResult{}, "Meta"),
cmpopts.IgnoreFields(mcp.ReadResourceResult{}, "Meta"),
}

View file

@ -2475,7 +2475,15 @@ func TestSetErrorPreservesContent(t *testing.T) {
} }
} }
var ctrCmpOpts = []cmp.Option{cmpopts.IgnoreUnexported(CallToolResult{}, GetPromptResult{}, ReadResourceResult{})} var ctrCmpOpts = []cmp.Option{
cmpopts.IgnoreUnexported(CallToolResult{}, GetPromptResult{}, ReadResourceResult{}),
// Server responses under the >= 2026-07-28 protocol carry an auto-populated
// [MetaKeyServerInfo] entry; tests that compare result bodies against
// hand-crafted expected values should ignore it.
cmpopts.IgnoreFields(CallToolResult{}, "Meta"),
cmpopts.IgnoreFields(GetPromptResult{}, "Meta"),
cmpopts.IgnoreFields(ReadResourceResult{}, "Meta"),
}
// runSubscriptionsListenTest exercises the SEP-2575 auto-listen flow end-to-end // runSubscriptionsListenTest exercises the SEP-2575 auto-listen flow end-to-end
// against the supplied transport pair. It captures every notification and the // against the supplied transport pair. It captures every notification and the

View file

@ -1143,8 +1143,6 @@ type DiscoverResult struct {
SupportedVersions []string `json:"supportedVersions"` SupportedVersions []string `json:"supportedVersions"`
// The server's capabilities. // The server's capabilities.
Capabilities *ServerCapabilities `json:"capabilities"` Capabilities *ServerCapabilities `json:"capabilities"`
// Information about the server implementation.
ServerInfo *Implementation `json:"serverInfo"`
// Instructions describing how to use the server and its features. // Instructions describing how to use the server and its features.
Instructions string `json:"instructions,omitempty"` Instructions string `json:"instructions,omitempty"`
} }
@ -2365,6 +2363,8 @@ const (
MetaKeyProtocolVersion = "io.modelcontextprotocol/protocolVersion" MetaKeyProtocolVersion = "io.modelcontextprotocol/protocolVersion"
// MetaKeyClientInfo carries the client's [Implementation]. // MetaKeyClientInfo carries the client's [Implementation].
MetaKeyClientInfo = "io.modelcontextprotocol/clientInfo" MetaKeyClientInfo = "io.modelcontextprotocol/clientInfo"
// MetaKeyServerInfo carries the server's [Implementation] on responses.
MetaKeyServerInfo = "io.modelcontextprotocol/serverInfo"
// MetaKeyClientCapabilities carries the client's [ClientCapabilities]. // MetaKeyClientCapabilities carries the client's [ClientCapabilities].
MetaKeyClientCapabilities = "io.modelcontextprotocol/clientCapabilities" MetaKeyClientCapabilities = "io.modelcontextprotocol/clientCapabilities"
// MetaKeyLogLevel identifies the desired log level for the request. // MetaKeyLogLevel identifies the desired log level for the request.

View file

@ -888,17 +888,21 @@ func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams]
if versions == nil { if versions == nil {
versions = slices.Clone(supportedProtocolVersions) versions = slices.Clone(supportedProtocolVersions)
} }
// Read the request-scoped identity/capabilities before acquiring the
// session lock: these accessors may fall back to Session.InitializeParams
// (which also locks Session.mu), so calling them from inside updateState
// would self-deadlock.
init := &InitializeParams{
ProtocolVersion: req.ProtocolVersion(),
Capabilities: req.ClientCapabilities(),
ClientInfo: req.ClientInfo(),
}
req.Session.updateState(func(state *ServerSessionState) { req.Session.updateState(func(state *ServerSessionState) {
state.InitializeParams = &InitializeParams{ state.InitializeParams = init
ProtocolVersion: req.ProtocolVersion(),
Capabilities: req.ClientCapabilities(),
ClientInfo: req.ClientInfo(),
}
}) })
res := &DiscoverResult{ res := &DiscoverResult{
SupportedVersions: versions, SupportedVersions: versions,
Capabilities: s.capabilities(), Capabilities: s.capabilities(),
ServerInfo: s.impl,
Instructions: s.opts.Instructions, Instructions: s.opts.Instructions,
} }
res.setDefaultCacheableValues() res.setDefaultCacheableValues()
@ -1924,10 +1928,32 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
} }
if validatedMeta.usesNewProtocol { if validatedMeta.usesNewProtocol {
setCompleteResultType(res) setCompleteResultType(res)
annotateServerInfo(res, ss.server.impl)
} }
return res, nil return res, nil
} }
// annotateServerInfo sets [MetaKeyServerInfo] on the result's `_meta` unless
// it is already present. Per SEP-2575, servers should identify themselves in
// each result's `_meta`.
func annotateServerInfo(res Result, impl *Implementation) {
if res == nil || impl == nil {
return
}
if _, isEmpty := res.(*emptyResult); isEmpty {
return
}
m := res.GetMeta()
if m == nil {
m = map[string]any{}
}
if _, ok := m[MetaKeyServerInfo]; ok {
return
}
m[MetaKeyServerInfo] = impl
res.SetMeta(m)
}
// InitializeParams returns the InitializeParams provided during the client's // InitializeParams returns the InitializeParams provided during the client's
// initial connection. // initial connection.
func (ss *ServerSession) InitializeParams() *InitializeParams { func (ss *ServerSession) InitializeParams() *InitializeParams {

View file

@ -535,9 +535,10 @@ type validatedMeta struct {
// the >= 2026-07-28 protocol via the `_meta` field. // the >= 2026-07-28 protocol via the `_meta` field.
// If the request has no _meta, or no protocolVersion in _meta, it returns a non-nil // If the request has no _meta, or no protocolVersion in _meta, it returns a non-nil
// validatedMeta with usesNewProtocol set to false, and a nil error. // validatedMeta with usesNewProtocol set to false, and a nil error.
// If the request has a protocolVersion in _meta it validates the presence of clientInfo // If the request has a protocolVersion in _meta it validates the presence of
// and clientCapabilities in _meta. If either is missing or invalid, it returns nil and // clientCapabilities in _meta. If it is missing or invalid, it returns nil and
// a non-nil error. Otherwise, it returns usesNewProtocol set to true and the populated // a non-nil error. clientInfo is optional; if present but invalid, an error is
// returned. Otherwise, it returns usesNewProtocol set to true and the populated
// initializeParams. // initializeParams.
func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) { func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
meta := extractRequestMeta(req.Params) meta := extractRequestMeta(req.Params)
@ -548,11 +549,15 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
if !ok || protocolVersion < protocolVersion20260728 { if !ok || protocolVersion < protocolVersion20260728 {
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
} }
clientInfo, ok := decodeMetaValue[*Implementation](meta, MetaKeyClientInfo) var clientInfo *Implementation
if !ok { if _, present := meta[MetaKeyClientInfo]; present {
return nil, &jsonrpc.Error{ var ok bool
Code: jsonrpc.CodeInvalidParams, clientInfo, ok = decodeMetaValue[*Implementation](meta, MetaKeyClientInfo)
Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientInfo), if !ok {
return nil, &jsonrpc.Error{
Code: jsonrpc.CodeInvalidParams,
Message: fmt.Sprintf("invalid _meta field %q", MetaKeyClientInfo),
}
} }
} }
capabilities, ok := decodeMetaValue[*clientCapabilitiesV2](meta, MetaKeyClientCapabilities) capabilities, ok := decodeMetaValue[*clientCapabilitiesV2](meta, MetaKeyClientCapabilities)

View file

@ -67,6 +67,19 @@ func TestValidateRequestMeta(t *testing.T) {
}, },
"name": "x", "name": "x",
}, },
wantUsesNew: true,
},
{
name: "new protocol invalid clientInfo",
method: methodCallTool,
params: map[string]any{
"_meta": map[string]any{
MetaKeyProtocolVersion: protocolVersion20260728,
MetaKeyClientInfo: "not an object",
MetaKeyClientCapabilities: map[string]any{},
},
"name": "x",
},
wantUsesNew: false, wantUsesNew: false,
wantErrContains: MetaKeyClientInfo, wantErrContains: MetaKeyClientInfo,
}, },

View file

@ -1265,11 +1265,13 @@ func TestStreamableClientOAuth_RetrieveError(t *testing.T) {
// discoverResult is the canned successful DiscoverResult returned by // discoverResult is the canned successful DiscoverResult returned by
// fakeStreamableServer setups in the tests below. // fakeStreamableServer setups in the tests below.
var discoverResult = &DiscoverResult{ var discoverResult = &DiscoverResult{
Meta: Meta{
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
},
SupportedVersions: []string{protocolVersion20260728}, SupportedVersions: []string{protocolVersion20260728},
Capabilities: &ServerCapabilities{ Capabilities: &ServerCapabilities{
Tools: &ToolCapabilities{ListChanged: true}, Tools: &ToolCapabilities{ListChanged: true},
}, },
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
Instructions: "test discover", Instructions: "test discover",
} }

View file

@ -794,6 +794,15 @@ func completeCallToolResult() *CallToolResult {
return r return r
} }
// completeCallToolResultWithServerInfo returns a completeCallToolResult
// annotated with the auto-populated [MetaKeyServerInfo] entry that servers
// add to every new-protocol response.
func completeCallToolResultWithServerInfo(impl *Implementation) *CallToolResult {
r := completeCallToolResult()
r.Meta = Meta{MetaKeyServerInfo: impl}
return r
}
func resp(id int64, result any, err error) *jsonrpc.Response { func resp(id int64, result any, err error) *jsonrpc.Response {
return &jsonrpc.Response{ return &jsonrpc.Response{
ID: jsonrpc2.Int64ID(id), ID: jsonrpc2.Int64ID(id),
@ -1958,6 +1967,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
MetaKeyClientInfo: map[string]any{"name": "testClient", "version": "v1.0.0"}, MetaKeyClientInfo: map[string]any{"name": "testClient", "version": "v1.0.0"},
MetaKeyClientCapabilities: map[string]any{}, MetaKeyClientCapabilities: map[string]any{},
} }
serverImpl := &Implementation{Name: "testServer", Version: "v1.0.0"}
testStreamableHandler(t, handler, []streamableRequest{ testStreamableHandler(t, handler, []streamableRequest{
{ {
@ -1969,7 +1979,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
}, },
messages: []jsonrpc.Message{req(2, "tools/call", &CallToolParams{Meta: testMeta, Name: "my-tool"})}, messages: []jsonrpc.Message{req(2, "tools/call", &CallToolParams{Meta: testMeta, Name: "my-tool"})},
wantStatusCode: http.StatusOK, wantStatusCode: http.StatusOK,
wantMessages: []jsonrpc.Message{resp(2, completeCallToolResult(), nil)}, wantMessages: []jsonrpc.Message{resp(2, completeCallToolResultWithServerInfo(serverImpl), nil)},
}, },
{ {
method: "POST", method: "POST",
@ -2013,7 +2023,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
}, },
messages: []jsonrpc.Message{req(6, "tools/call", &CallToolParams{Meta: testMeta, Name: "my-tool"})}, messages: []jsonrpc.Message{req(6, "tools/call", &CallToolParams{Meta: testMeta, Name: "my-tool"})},
wantStatusCode: http.StatusOK, wantStatusCode: http.StatusOK,
wantMessages: []jsonrpc.Message{resp(6, completeCallToolResult(), nil)}, wantMessages: []jsonrpc.Message{resp(6, completeCallToolResultWithServerInfo(serverImpl), nil)},
}, },
{ {
method: "POST", method: "POST",
@ -2029,7 +2039,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
Arguments: map[string]any{"region": "us-west1", "query": "SELECT 1"}, Arguments: map[string]any{"region": "us-west1", "query": "SELECT 1"},
})}, })},
wantStatusCode: http.StatusOK, wantStatusCode: http.StatusOK,
wantMessages: []jsonrpc.Message{resp(7, completeCallToolResult(), nil)}, wantMessages: []jsonrpc.Message{resp(7, completeCallToolResultWithServerInfo(serverImpl), nil)},
}, },
{ {
method: "POST", method: "POST",

View file

@ -1,7 +1,7 @@
The client sends a discover request The client sends a discover request carrying the per-request _meta
carrying the required per-request _meta triple (protocolVersion, clientInfo, (protocolVersion, clientInfo, clientCapabilities), and the server
clientCapabilities), and the server replies with the protocol versions it replies with the protocol versions it supports, its capabilities, and its
supports, its capabilities, and its identity. identity via `_meta.io.modelcontextprotocol/serverInfo`.
-- client -- -- client --
{ {
@ -22,6 +22,12 @@ supports, its capabilities, and its identity.
"id": 1, "id": 1,
"result": { "result": {
"resultType": "complete", "resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "testServer",
"version": "v1.0.0"
}
},
"ttlMs": 0, "ttlMs": 0,
"cacheScope": "public", "cacheScope": "public",
"supportedVersions": [ "supportedVersions": [
@ -33,10 +39,6 @@ supports, its capabilities, and its identity.
], ],
"capabilities": { "capabilities": {
"logging": {} "logging": {}
},
"serverInfo": {
"name": "testServer",
"version": "v1.0.0"
} }
} }
} }

View file

@ -45,6 +45,12 @@ confirmThenGreet
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 1, "id": 1,
"result": { "result": {
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "testServer",
"version": "v1.0.0"
}
},
"content": null, "content": null,
"requestState": "step=1", "requestState": "step=1",
"resultType": "input_required", "resultType": "input_required",
@ -71,6 +77,12 @@ confirmThenGreet
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": 2, "id": 2,
"result": { "result": {
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "testServer",
"version": "v1.0.0"
}
},
"content": [ "content": [
{ {
"type": "text", "type": "text",

View file

@ -42,7 +42,7 @@ func ExampleLoggingTransport() {
} }
// Output: // Output:
// read: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}},"serverInfo":{"name":"server","version":"v0.0.1"}}} // read: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"server","version":"v0.0.1"}},"ttlMs":0,"cacheScope":"public","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"capabilities":{"logging":{}}}}
// write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}} // write: {"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/clientCapabilities":{"roots":{"listChanged":true}},"io.modelcontextprotocol/clientInfo":{"name":"client","version":"v0.0.1"},"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}
} }