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:
parent
fa548d7401
commit
5cb8339210
17 changed files with 210 additions and 68 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"}}}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
18
mcp/testdata/conformance/server/discover.txtar
vendored
18
mcp/testdata/conformance/server/discover.txtar
vendored
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
mcp/testdata/conformance/server/mrtr.txtar
vendored
12
mcp/testdata/conformance/server/mrtr.txtar
vendored
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue