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
13 changed files with 210 additions and 68 deletions
|
|
@ -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{
|
||||
Capabilities: res.Capabilities,
|
||||
Instructions: res.Instructions,
|
||||
ProtocolVersion: negotiated,
|
||||
ServerInfo: res.ServerInfo,
|
||||
ServerInfo: serverInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -509,9 +513,12 @@ func (cs *ClientSession) usesNewProtocol() bool {
|
|||
return res != nil && res.ProtocolVersion >= protocolVersion20260728
|
||||
}
|
||||
|
||||
// injectRequestMeta populates the SEP-2575 per-request `_meta` triple
|
||||
// (protocolVersion, clientInfo, clientCapabilities) on the given outgoing
|
||||
// request params. Keys already present in params.Meta are not overwritten.
|
||||
// injectRequestMeta populates the SEP-2575 per-request `_meta` fields
|
||||
// (protocolVersion, optional clientInfo, clientCapabilities) on the given
|
||||
// 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 {
|
||||
*T
|
||||
Params
|
||||
|
|
@ -527,7 +534,7 @@ func injectRequestMeta[T any, P interface {
|
|||
if _, ok := m[MetaKeyProtocolVersion]; !ok {
|
||||
m[MetaKeyProtocolVersion] = res.ProtocolVersion
|
||||
}
|
||||
if _, ok := m[MetaKeyClientInfo]; !ok {
|
||||
if _, ok := m[MetaKeyClientInfo]; !ok && cs.client.impl != nil {
|
||||
m[MetaKeyClientInfo] = cs.client.impl
|
||||
}
|
||||
if _, ok := m[MetaKeyClientCapabilities]; !ok {
|
||||
|
|
|
|||
|
|
@ -662,11 +662,13 @@ func TestClientConnectDiscover(t *testing.T) {
|
|||
name: "discover success skips initialize",
|
||||
discoverHandler: func() (Result, error) {
|
||||
return &DiscoverResult{
|
||||
Meta: Meta{
|
||||
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
|
||||
},
|
||||
SupportedVersions: []string{protocolVersion20260728},
|
||||
Capabilities: &ServerCapabilities{
|
||||
Tools: &ToolCapabilities{ListChanged: true},
|
||||
},
|
||||
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
|
||||
}, nil
|
||||
},
|
||||
wantInitialize: false,
|
||||
|
|
@ -695,9 +697,11 @@ func TestClientConnectDiscover(t *testing.T) {
|
|||
name: "no overlapping supported version falls back to initialize",
|
||||
discoverHandler: func() (Result, error) {
|
||||
return &DiscoverResult{
|
||||
Meta: Meta{
|
||||
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
|
||||
},
|
||||
SupportedVersions: []string{otherVersionsOnly},
|
||||
Capabilities: &ServerCapabilities{},
|
||||
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
|
||||
}, nil
|
||||
},
|
||||
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) {
|
||||
if method == methodDiscover {
|
||||
return &DiscoverResult{
|
||||
Meta: Meta{
|
||||
MetaKeyServerInfo: &Implementation{Name: "vpre-like-server", Version: "v1"},
|
||||
},
|
||||
SupportedVersions: []string{protocolVersion20251125},
|
||||
Capabilities: &ServerCapabilities{},
|
||||
ServerInfo: &Implementation{Name: "vpre-like-server", Version: "v1"},
|
||||
}, nil
|
||||
}
|
||||
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
|
||||
// against the supplied transport pair. It captures every notification and the
|
||||
|
|
|
|||
|
|
@ -1143,8 +1143,6 @@ type DiscoverResult struct {
|
|||
SupportedVersions []string `json:"supportedVersions"`
|
||||
// The server's 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 string `json:"instructions,omitempty"`
|
||||
}
|
||||
|
|
@ -2365,6 +2363,8 @@ const (
|
|||
MetaKeyProtocolVersion = "io.modelcontextprotocol/protocolVersion"
|
||||
// MetaKeyClientInfo carries the client's [Implementation].
|
||||
MetaKeyClientInfo = "io.modelcontextprotocol/clientInfo"
|
||||
// MetaKeyServerInfo carries the server's [Implementation] on responses.
|
||||
MetaKeyServerInfo = "io.modelcontextprotocol/serverInfo"
|
||||
// MetaKeyClientCapabilities carries the client's [ClientCapabilities].
|
||||
MetaKeyClientCapabilities = "io.modelcontextprotocol/clientCapabilities"
|
||||
// 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 {
|
||||
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) {
|
||||
state.InitializeParams = &InitializeParams{
|
||||
ProtocolVersion: req.ProtocolVersion(),
|
||||
Capabilities: req.ClientCapabilities(),
|
||||
ClientInfo: req.ClientInfo(),
|
||||
}
|
||||
state.InitializeParams = init
|
||||
})
|
||||
res := &DiscoverResult{
|
||||
SupportedVersions: versions,
|
||||
Capabilities: s.capabilities(),
|
||||
ServerInfo: s.impl,
|
||||
Instructions: s.opts.Instructions,
|
||||
}
|
||||
res.setDefaultCacheableValues()
|
||||
|
|
@ -1924,10 +1928,32 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
|
|||
}
|
||||
if validatedMeta.usesNewProtocol {
|
||||
setCompleteResultType(res)
|
||||
annotateServerInfo(res, ss.server.impl)
|
||||
}
|
||||
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
|
||||
// initial connection.
|
||||
func (ss *ServerSession) InitializeParams() *InitializeParams {
|
||||
|
|
|
|||
|
|
@ -535,9 +535,10 @@ type validatedMeta struct {
|
|||
// 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
|
||||
// validatedMeta with usesNewProtocol set to false, and a nil error.
|
||||
// If the request has a protocolVersion in _meta it validates the presence of clientInfo
|
||||
// and clientCapabilities in _meta. If either is missing or invalid, it returns nil and
|
||||
// a non-nil error. Otherwise, it returns usesNewProtocol set to true and the populated
|
||||
// If the request has a protocolVersion in _meta it validates the presence of
|
||||
// clientCapabilities in _meta. If it is missing or invalid, it returns nil and
|
||||
// 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.
|
||||
func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
|
||||
meta := extractRequestMeta(req.Params)
|
||||
|
|
@ -548,11 +549,15 @@ func validateRequestMeta(req *jsonrpc.Request) (*validatedMeta, error) {
|
|||
if !ok || protocolVersion < protocolVersion20260728 {
|
||||
return &validatedMeta{usesNewProtocol: false, initializeParams: nil}, nil
|
||||
}
|
||||
clientInfo, ok := decodeMetaValue[*Implementation](meta, MetaKeyClientInfo)
|
||||
if !ok {
|
||||
return nil, &jsonrpc.Error{
|
||||
Code: jsonrpc.CodeInvalidParams,
|
||||
Message: fmt.Sprintf("missing or invalid _meta field %q", MetaKeyClientInfo),
|
||||
var clientInfo *Implementation
|
||||
if _, present := meta[MetaKeyClientInfo]; present {
|
||||
var ok bool
|
||||
clientInfo, ok = decodeMetaValue[*Implementation](meta, MetaKeyClientInfo)
|
||||
if !ok {
|
||||
return nil, &jsonrpc.Error{
|
||||
Code: jsonrpc.CodeInvalidParams,
|
||||
Message: fmt.Sprintf("invalid _meta field %q", MetaKeyClientInfo),
|
||||
}
|
||||
}
|
||||
}
|
||||
capabilities, ok := decodeMetaValue[*clientCapabilitiesV2](meta, MetaKeyClientCapabilities)
|
||||
|
|
|
|||
|
|
@ -67,6 +67,19 @@ func TestValidateRequestMeta(t *testing.T) {
|
|||
},
|
||||
"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,
|
||||
wantErrContains: MetaKeyClientInfo,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1265,11 +1265,13 @@ func TestStreamableClientOAuth_RetrieveError(t *testing.T) {
|
|||
// discoverResult is the canned successful DiscoverResult returned by
|
||||
// fakeStreamableServer setups in the tests below.
|
||||
var discoverResult = &DiscoverResult{
|
||||
Meta: Meta{
|
||||
MetaKeyServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
|
||||
},
|
||||
SupportedVersions: []string{protocolVersion20260728},
|
||||
Capabilities: &ServerCapabilities{
|
||||
Tools: &ToolCapabilities{ListChanged: true},
|
||||
},
|
||||
ServerInfo: &Implementation{Name: "discoverServer", Version: "v1.0.0"},
|
||||
Instructions: "test discover",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -794,6 +794,15 @@ func completeCallToolResult() *CallToolResult {
|
|||
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 {
|
||||
return &jsonrpc.Response{
|
||||
ID: jsonrpc2.Int64ID(id),
|
||||
|
|
@ -1958,6 +1967,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
|
|||
MetaKeyClientInfo: map[string]any{"name": "testClient", "version": "v1.0.0"},
|
||||
MetaKeyClientCapabilities: map[string]any{},
|
||||
}
|
||||
serverImpl := &Implementation{Name: "testServer", Version: "v1.0.0"}
|
||||
|
||||
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"})},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantMessages: []jsonrpc.Message{resp(2, completeCallToolResult(), nil)},
|
||||
wantMessages: []jsonrpc.Message{resp(2, completeCallToolResultWithServerInfo(serverImpl), nil)},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
|
|
@ -2013,7 +2023,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
|
|||
},
|
||||
messages: []jsonrpc.Message{req(6, "tools/call", &CallToolParams{Meta: testMeta, Name: "my-tool"})},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantMessages: []jsonrpc.Message{resp(6, completeCallToolResult(), nil)},
|
||||
wantMessages: []jsonrpc.Message{resp(6, completeCallToolResultWithServerInfo(serverImpl), nil)},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
|
|
@ -2029,7 +2039,7 @@ func TestStreamableMcpHeaderValidation(t *testing.T) {
|
|||
Arguments: map[string]any{"region": "us-west1", "query": "SELECT 1"},
|
||||
})},
|
||||
wantStatusCode: http.StatusOK,
|
||||
wantMessages: []jsonrpc.Message{resp(7, completeCallToolResult(), nil)},
|
||||
wantMessages: []jsonrpc.Message{resp(7, completeCallToolResultWithServerInfo(serverImpl), nil)},
|
||||
},
|
||||
{
|
||||
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
|
||||
carrying the required per-request _meta triple (protocolVersion, clientInfo,
|
||||
clientCapabilities), and the server replies with the protocol versions it
|
||||
supports, its capabilities, and its identity.
|
||||
The client sends a discover request carrying the per-request _meta
|
||||
(protocolVersion, clientInfo, clientCapabilities), and the server
|
||||
replies with the protocol versions it supports, its capabilities, and its
|
||||
identity via `_meta.io.modelcontextprotocol/serverInfo`.
|
||||
|
||||
-- client --
|
||||
{
|
||||
|
|
@ -22,6 +22,12 @@ supports, its capabilities, and its identity.
|
|||
"id": 1,
|
||||
"result": {
|
||||
"resultType": "complete",
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/serverInfo": {
|
||||
"name": "testServer",
|
||||
"version": "v1.0.0"
|
||||
}
|
||||
},
|
||||
"ttlMs": 0,
|
||||
"cacheScope": "public",
|
||||
"supportedVersions": [
|
||||
|
|
@ -33,10 +39,6 @@ supports, its capabilities, and its identity.
|
|||
],
|
||||
"capabilities": {
|
||||
"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",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/serverInfo": {
|
||||
"name": "testServer",
|
||||
"version": "v1.0.0"
|
||||
}
|
||||
},
|
||||
"content": null,
|
||||
"requestState": "step=1",
|
||||
"resultType": "input_required",
|
||||
|
|
@ -71,6 +77,12 @@ confirmThenGreet
|
|||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"_meta": {
|
||||
"io.modelcontextprotocol/serverInfo": {
|
||||
"name": "testServer",
|
||||
"version": "v1.0.0"
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func ExampleLoggingTransport() {
|
|||
}
|
||||
|
||||
// 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"}}}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue