Skip to content

Commit 5cb8339

Browse files
mcp: update return meta in DiscoverResult (SEP-2575) (#1097)
This PR implements the latest changes introduced by modelcontextprotocol/modelcontextprotocol#3002
1 parent fa548d7 commit 5cb8339

17 files changed

Lines changed: 210 additions & 68 deletions

conformance/baseline.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
server: [] # All tests pass!
1+
server:
2+
# The `server-stateless` scenario ships with the `@modelcontextprotocol/conformance@0.2.0-alpha.9`
3+
# suite pinned by the workflow, which still enforces the pre-PR-3002
4+
# (https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002) spec:
5+
# - `clientInfo` in the per-request `_meta` is required (MUST).
6+
# - `serverInfo` is a body field on `DiscoverResult`.
7+
# PR #3002 relaxes `clientInfo` to SHOULD and moves `serverInfo` into every
8+
# response's `_meta`. The Go SDK follows the updated spec, so the following
9+
# checks in the `server-stateless` scenario fail against the pinned suite:
10+
# - sep-2575-request-meta-invalid-missing-client-info
11+
# - sep-2575-http-server-meta-invalid-400
12+
# - sep-2575-server-implements-discover
13+
# Remove this entry when the conformance suite is bumped to a release that
14+
# incorporates PR #3002.
15+
- server-stateless
216

317
client:
4-
# OAuth mechanisms not yet implemented by the Go MCP SDK.
18+
# OAuth mechanisms not implemented by the Go MCP SDK.
519
- auth/client-credentials-basic
620
- auth/client-credentials-jwt
721
- auth/enterprise-managed-authorization
@@ -10,4 +24,6 @@ client:
1024
# server/discover handler advertises "2026-07-28" but delegates every
1125
# other method to a pinned @modelcontextprotocol/sdk that rejects that
1226
# version at the HTTP layer.
27+
# Remove this entry when the conformance suite is bumped to a release that
28+
# fixes this issue.
1329
- json-schema-ref-no-deref

docs/protocol.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
1. [Lifecycle](#lifecycle)
55
1. [Discovery (`server/discover`)](#discovery-(server/discover))
66
1. [Per-request `_meta` keys](#per-request-meta-keys)
7+
1. [Per-response `_meta` keys](#per-response-meta-keys)
78
1. [Subscriptions (`subscriptions/listen`)](#subscriptions-(subscriptions/listen))
89
1. [Transports](#transports)
910
1. [Stdio Transport](#stdio-transport)
@@ -40,8 +41,10 @@ transparently based on the negotiated protocol version:
4041
- A **stateless** model introduced in `2026-07-28` by
4142
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575),
4243
in which there is no `initialize`/`notifications/initialized` handshake, and
43-
each request carries its protocol version, client identity, and client
44-
capabilities in `_meta`.
44+
each request carries its protocol version and client capabilities in
45+
`_meta`. Clients SHOULD also include their identity (`clientInfo`) on every
46+
request, and servers SHOULD include their identity (`serverInfo`) on every
47+
response.
4548

4649
In both models, the SDK exposes the same API:
4750

@@ -130,17 +133,28 @@ When the negotiated protocol version is `2026-07-28` or later, every request
130133
carries these keys inside its `_meta` map (constants live in
131134
[`mcp/protocol.go`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#pkg-constants)):
132135

133-
| Constant | Wire key | Type |
134-
|---|---|---|
135-
| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` |
136-
| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` |
137-
| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` |
138-
| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) |
139-
140-
The client populates these keys automatically on every outgoing request.
141-
Server-side handlers can read them
142-
through `ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`,
143-
and `ServerRequest[P].ClientCapabilities()`.
136+
| Constant | Wire key | Type | Required |
137+
|---|---|---|---|
138+
| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` | Yes |
139+
| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` | Yes |
140+
| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` | No |
141+
| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) | No |
142+
143+
The client populates the required keys automatically on every outgoing
144+
request, and populates `clientInfo` when configured with an `*Implementation`
145+
(the default). Server-side handlers can read them through
146+
`ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, and
147+
`ServerRequest[P].ClientCapabilities()`.
148+
149+
### Per-response `_meta` keys
150+
151+
Under the same protocol version, servers SHOULD identify themselves on every
152+
response. The SDK populates this
153+
key automatically on every outgoing response:
154+
155+
| Constant | Wire key | Type | Required |
156+
|---|---|---|---|
157+
| `MetaKeyServerInfo` | `io.modelcontextprotocol/serverInfo` | `*Implementation` | No |
144158

145159
### Subscriptions (`subscriptions/listen`)
146160

docs/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func ExampleLoggingTransport() {
5050
}
5151

5252
// Output:
53-
// 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"}}}
53+
// 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":{}}}}
5454
// 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"}}}
5555
}
5656
```

internal/docs/protocol.src.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ transparently based on the negotiated protocol version:
1717
- A **stateless** model introduced in `2026-07-28` by
1818
[SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575),
1919
in which there is no `initialize`/`notifications/initialized` handshake, and
20-
each request carries its protocol version, client identity, and client
21-
capabilities in `_meta`.
20+
each request carries its protocol version and client capabilities in
21+
`_meta`. Clients SHOULD also include their identity (`clientInfo`) on every
22+
request, and servers SHOULD include their identity (`serverInfo`) on every
23+
response.
2224

2325
In both models, the SDK exposes the same API:
2426

@@ -76,17 +78,28 @@ When the negotiated protocol version is `2026-07-28` or later, every request
7678
carries these keys inside its `_meta` map (constants live in
7779
[`mcp/protocol.go`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#pkg-constants)):
7880

79-
| Constant | Wire key | Type |
80-
|---|---|---|
81-
| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` |
82-
| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` |
83-
| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` |
84-
| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) |
85-
86-
The client populates these keys automatically on every outgoing request.
87-
Server-side handlers can read them
88-
through `ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`,
89-
and `ServerRequest[P].ClientCapabilities()`.
81+
| Constant | Wire key | Type | Required |
82+
|---|---|---|---|
83+
| `MetaKeyProtocolVersion` | `io.modelcontextprotocol/protocolVersion` | `string` | Yes |
84+
| `MetaKeyClientCapabilities` | `io.modelcontextprotocol/clientCapabilities` | `*ClientCapabilities` | Yes |
85+
| `MetaKeyClientInfo` | `io.modelcontextprotocol/clientInfo` | `*Implementation` | No |
86+
| `MetaKeyLogLevel` | `io.modelcontextprotocol/logLevel` | `LoggingLevel` (deprecated by SEP-2577) | No |
87+
88+
The client populates the required keys automatically on every outgoing
89+
request, and populates `clientInfo` when configured with an `*Implementation`
90+
(the default). Server-side handlers can read them through
91+
`ServerRequest[P].ProtocolVersion()`, `ServerRequest[P].ClientInfo()`, and
92+
`ServerRequest[P].ClientCapabilities()`.
93+
94+
### Per-response `_meta` keys
95+
96+
Under the same protocol version, servers SHOULD identify themselves on every
97+
response. The SDK populates this
98+
key automatically on every outgoing response:
99+
100+
| Constant | Wire key | Type | Required |
101+
|---|---|---|---|
102+
| `MetaKeyServerInfo` | `io.modelcontextprotocol/serverInfo` | `*Implementation` | No |
90103

91104
### Subscriptions (`subscriptions/listen`)
92105

mcp/client.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -443,11 +443,15 @@ func (c *Client) discover(ctx context.Context, cs *ClientSession) (*InitializeRe
443443
}
444444
}
445445

446+
var serverInfo *Implementation
447+
if v, ok := decodeMetaValue[*Implementation](res.GetMeta(), MetaKeyServerInfo); ok {
448+
serverInfo = v
449+
}
446450
return &InitializeResult{
447451
Capabilities: res.Capabilities,
448452
Instructions: res.Instructions,
449453
ProtocolVersion: negotiated,
450-
ServerInfo: res.ServerInfo,
454+
ServerInfo: serverInfo,
451455
}, nil
452456
}
453457

@@ -509,9 +513,12 @@ func (cs *ClientSession) usesNewProtocol() bool {
509513
return res != nil && res.ProtocolVersion >= protocolVersion20260728
510514
}
511515

512-
// injectRequestMeta populates the SEP-2575 per-request `_meta` triple
513-
// (protocolVersion, clientInfo, clientCapabilities) on the given outgoing
514-
// request params. Keys already present in params.Meta are not overwritten.
516+
// injectRequestMeta populates the SEP-2575 per-request `_meta` fields
517+
// (protocolVersion, optional clientInfo, clientCapabilities) on the given
518+
// outgoing request params. Keys already present in params.Meta are not
519+
// overwritten. Per PR modelcontextprotocol/modelcontextprotocol#3002
520+
// clientInfo is SHOULD (not MUST), and is omitted when the client has no
521+
// [Implementation] configured.
515522
func injectRequestMeta[T any, P interface {
516523
*T
517524
Params
@@ -527,7 +534,7 @@ func injectRequestMeta[T any, P interface {
527534
if _, ok := m[MetaKeyProtocolVersion]; !ok {
528535
m[MetaKeyProtocolVersion] = res.ProtocolVersion
529536
}
530-
if _, ok := m[MetaKeyClientInfo]; !ok {
537+
if _, ok := m[MetaKeyClientInfo]; !ok && cs.client.impl != nil {
531538
m[MetaKeyClientInfo] = cs.client.impl
532539
}
533540
if _, ok := m[MetaKeyClientCapabilities]; !ok {

mcp/client_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,11 +662,13 @@ func TestClientConnectDiscover(t *testing.T) {
662662
name: "discover success skips initialize",
663663
discoverHandler: func() (Result, error) {
664664
return &DiscoverResult{
665+
Meta: Meta{
666+
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
667+
},
665668
SupportedVersions: []string{protocolVersion20260728},
666669
Capabilities: &ServerCapabilities{
667670
Tools: &ToolCapabilities{ListChanged: true},
668671
},
669-
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
670672
}, nil
671673
},
672674
wantInitialize: false,
@@ -695,9 +697,11 @@ func TestClientConnectDiscover(t *testing.T) {
695697
name: "no overlapping supported version falls back to initialize",
696698
discoverHandler: func() (Result, error) {
697699
return &DiscoverResult{
700+
Meta: Meta{
701+
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
702+
},
698703
SupportedVersions: []string{otherVersionsOnly},
699704
Capabilities: &ServerCapabilities{},
700-
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
701705
}, nil
702706
},
703707
wantInitialize: true,
@@ -885,9 +889,11 @@ func TestInMemory_E2E_DiscoverFallback_NoOverlap(t *testing.T) {
885889
return func(ctx context.Context, method string, req Request) (Result, error) {
886890
if method == methodDiscover {
887891
return &DiscoverResult{
892+
Meta: Meta{
893+
MetaKeyServerInfo: &Implementation{Name: "vpre-like-server", Version: "v1"},
894+
},
888895
SupportedVersions: []string{protocolVersion20251125},
889896
Capabilities: &ServerCapabilities{},
890-
ServerInfo: &Implementation{Name: "vpre-like-server", Version: "v1"},
891897
}, nil
892898
}
893899
return next(ctx, method, req)

mcp/content_nil_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,12 @@ func TestContentUnmarshalNilWithInvalidContent(t *testing.T) {
224224
}
225225
}
226226

227-
var ctrCmpOpts = []cmp.Option{cmpopts.IgnoreUnexported(mcp.CallToolResult{}, mcp.GetPromptResult{}, mcp.ReadResourceResult{})}
227+
var ctrCmpOpts = []cmp.Option{
228+
cmpopts.IgnoreUnexported(mcp.CallToolResult{}, mcp.GetPromptResult{}, mcp.ReadResourceResult{}),
229+
// Server responses under the >= 2026-07-28 protocol carry an auto-populated
230+
// [mcp.MetaKeyServerInfo] entry; tests that compare result bodies against
231+
// hand-crafted expected values should ignore it.
232+
cmpopts.IgnoreFields(mcp.CallToolResult{}, "Meta"),
233+
cmpopts.IgnoreFields(mcp.GetPromptResult{}, "Meta"),
234+
cmpopts.IgnoreFields(mcp.ReadResourceResult{}, "Meta"),
235+
}

mcp/mcp_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2475,7 +2475,15 @@ func TestSetErrorPreservesContent(t *testing.T) {
24752475
}
24762476
}
24772477

2478-
var ctrCmpOpts = []cmp.Option{cmpopts.IgnoreUnexported(CallToolResult{}, GetPromptResult{}, ReadResourceResult{})}
2478+
var ctrCmpOpts = []cmp.Option{
2479+
cmpopts.IgnoreUnexported(CallToolResult{}, GetPromptResult{}, ReadResourceResult{}),
2480+
// Server responses under the >= 2026-07-28 protocol carry an auto-populated
2481+
// [MetaKeyServerInfo] entry; tests that compare result bodies against
2482+
// hand-crafted expected values should ignore it.
2483+
cmpopts.IgnoreFields(CallToolResult{}, "Meta"),
2484+
cmpopts.IgnoreFields(GetPromptResult{}, "Meta"),
2485+
cmpopts.IgnoreFields(ReadResourceResult{}, "Meta"),
2486+
}
24792487

24802488
// runSubscriptionsListenTest exercises the SEP-2575 auto-listen flow end-to-end
24812489
// against the supplied transport pair. It captures every notification and the

mcp/protocol.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,8 +1143,6 @@ type DiscoverResult struct {
11431143
SupportedVersions []string `json:"supportedVersions"`
11441144
// The server's capabilities.
11451145
Capabilities *ServerCapabilities `json:"capabilities"`
1146-
// Information about the server implementation.
1147-
ServerInfo *Implementation `json:"serverInfo"`
11481146
// Instructions describing how to use the server and its features.
11491147
Instructions string `json:"instructions,omitempty"`
11501148
}
@@ -2365,6 +2363,8 @@ const (
23652363
MetaKeyProtocolVersion = "io.modelcontextprotocol/protocolVersion"
23662364
// MetaKeyClientInfo carries the client's [Implementation].
23672365
MetaKeyClientInfo = "io.modelcontextprotocol/clientInfo"
2366+
// MetaKeyServerInfo carries the server's [Implementation] on responses.
2367+
MetaKeyServerInfo = "io.modelcontextprotocol/serverInfo"
23682368
// MetaKeyClientCapabilities carries the client's [ClientCapabilities].
23692369
MetaKeyClientCapabilities = "io.modelcontextprotocol/clientCapabilities"
23702370
// MetaKeyLogLevel identifies the desired log level for the request.

mcp/server.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -888,17 +888,21 @@ func (s *Server) discover(_ context.Context, req *ServerRequest[*DiscoverParams]
888888
if versions == nil {
889889
versions = slices.Clone(supportedProtocolVersions)
890890
}
891+
// Read the request-scoped identity/capabilities before acquiring the
892+
// session lock: these accessors may fall back to Session.InitializeParams
893+
// (which also locks Session.mu), so calling them from inside updateState
894+
// would self-deadlock.
895+
init := &InitializeParams{
896+
ProtocolVersion: req.ProtocolVersion(),
897+
Capabilities: req.ClientCapabilities(),
898+
ClientInfo: req.ClientInfo(),
899+
}
891900
req.Session.updateState(func(state *ServerSessionState) {
892-
state.InitializeParams = &InitializeParams{
893-
ProtocolVersion: req.ProtocolVersion(),
894-
Capabilities: req.ClientCapabilities(),
895-
ClientInfo: req.ClientInfo(),
896-
}
901+
state.InitializeParams = init
897902
})
898903
res := &DiscoverResult{
899904
SupportedVersions: versions,
900905
Capabilities: s.capabilities(),
901-
ServerInfo: s.impl,
902906
Instructions: s.opts.Instructions,
903907
}
904908
res.setDefaultCacheableValues()
@@ -1924,10 +1928,32 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
19241928
}
19251929
if validatedMeta.usesNewProtocol {
19261930
setCompleteResultType(res)
1931+
annotateServerInfo(res, ss.server.impl)
19271932
}
19281933
return res, nil
19291934
}
19301935

1936+
// annotateServerInfo sets [MetaKeyServerInfo] on the result's `_meta` unless
1937+
// it is already present. Per SEP-2575, servers should identify themselves in
1938+
// each result's `_meta`.
1939+
func annotateServerInfo(res Result, impl *Implementation) {
1940+
if res == nil || impl == nil {
1941+
return
1942+
}
1943+
if _, isEmpty := res.(*emptyResult); isEmpty {
1944+
return
1945+
}
1946+
m := res.GetMeta()
1947+
if m == nil {
1948+
m = map[string]any{}
1949+
}
1950+
if _, ok := m[MetaKeyServerInfo]; ok {
1951+
return
1952+
}
1953+
m[MetaKeyServerInfo] = impl
1954+
res.SetMeta(m)
1955+
}
1956+
19311957
// InitializeParams returns the InitializeParams provided during the client's
19321958
// initial connection.
19331959
func (ss *ServerSession) InitializeParams() *InitializeParams {

0 commit comments

Comments
 (0)