mcp: include resultType on new-protocol responses (#1060)
## Summary - Add `resultType: "complete"` to non-MRTR server result responses for 2026-07-28 requests. - Cover `server/discover`, list methods, and completion responses, and update expected conformance/example output. ## Why Fixes #1032. The 2026-07-28 schema requires `resultType` on result objects, but only the MRTR-capable results were setting it. ## Validation - `go test ./mcp -run 'Test(ServerSessionHandle|CompleteResult|ClientConnectDiscover|InMemory_E2E_Discover)' -count=1` — passed\n- `go test ./mcp -count=1` — passed\n- `go test ./...` — passed\n- `go vet ./...` — passed --------- Co-authored-by: ychampion <ychampion@users.noreply.github.com> Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
This commit is contained in:
parent
40f034322f
commit
dd79ca3d44
6 changed files with 178 additions and 3 deletions
|
|
@ -28,6 +28,24 @@ const (
|
||||||
resultTypeInputRequired resultType = "input_required"
|
resultTypeInputRequired resultType = "input_required"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type completeResultWithType struct {
|
||||||
|
ResultType resultType `json:"resultType,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *completeResultWithType) setResultType(rt resultType) { r.ResultType = rt }
|
||||||
|
func (*completeResultWithType) isCompleteResult() {}
|
||||||
|
|
||||||
|
type completeResultResponse interface {
|
||||||
|
setResultType(resultType)
|
||||||
|
isCompleteResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCompleteResultType(res Result) {
|
||||||
|
if r, ok := res.(completeResultResponse); ok {
|
||||||
|
r.setResultType(resultTypeComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// InputRequest is a type for parameters that a server can include in the response
|
// InputRequest is a type for parameters that a server can include in the response
|
||||||
// to request input from client (SEP-2322). Implementations are [*ElicitParams],
|
// to request input from client (SEP-2322). Implementations are [*ElicitParams],
|
||||||
// [*CreateMessageParams], and [*ListRootsParams].
|
// [*CreateMessageParams], and [*ListRootsParams].
|
||||||
|
|
@ -640,6 +658,7 @@ type CompletionResultDetails struct {
|
||||||
|
|
||||||
// The server's response to a completion/complete request
|
// The server's response to a completion/complete request
|
||||||
type CompleteResult struct {
|
type CompleteResult struct {
|
||||||
|
completeResultWithType
|
||||||
// This property is reserved by the protocol to allow clients and servers to
|
// This property is reserved by the protocol to allow clients and servers to
|
||||||
// attach additional metadata to their responses.
|
// attach additional metadata to their responses.
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
|
|
@ -1117,6 +1136,7 @@ func (x *DiscoverParams) GetProgressToken() any { return getProgressToken(x) }
|
||||||
func (x *DiscoverParams) SetProgressToken(t any) { setProgressToken(x, t) }
|
func (x *DiscoverParams) SetProgressToken(t any) { setProgressToken(x, t) }
|
||||||
|
|
||||||
type DiscoverResult struct {
|
type DiscoverResult struct {
|
||||||
|
completeResultWithType
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
Cacheable
|
Cacheable
|
||||||
// The versions of the Model Context Protocol that the server supports.
|
// The versions of the Model Context Protocol that the server supports.
|
||||||
|
|
@ -1180,6 +1200,7 @@ func (c *Cacheable) setDefaultCacheableValues() {
|
||||||
|
|
||||||
// The server's response to a prompts/list request from the client.
|
// The server's response to a prompts/list request from the client.
|
||||||
type ListPromptsResult struct {
|
type ListPromptsResult struct {
|
||||||
|
completeResultWithType
|
||||||
// This property is reserved by the protocol to allow clients and servers to
|
// This property is reserved by the protocol to allow clients and servers to
|
||||||
// attach additional metadata to their responses.
|
// attach additional metadata to their responses.
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
|
|
@ -1210,6 +1231,7 @@ func (x *ListResourceTemplatesParams) cursorPtr() *string { return &x.Cursor
|
||||||
|
|
||||||
// The server's response to a resources/templates/list request from the client.
|
// The server's response to a resources/templates/list request from the client.
|
||||||
type ListResourceTemplatesResult struct {
|
type ListResourceTemplatesResult struct {
|
||||||
|
completeResultWithType
|
||||||
// This property is reserved by the protocol to allow clients and servers to
|
// This property is reserved by the protocol to allow clients and servers to
|
||||||
// attach additional metadata to their responses.
|
// attach additional metadata to their responses.
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
|
|
@ -1240,6 +1262,7 @@ func (x *ListResourcesParams) cursorPtr() *string { return &x.Cursor }
|
||||||
|
|
||||||
// The server's response to a resources/list request from the client.
|
// The server's response to a resources/list request from the client.
|
||||||
type ListResourcesResult struct {
|
type ListResourcesResult struct {
|
||||||
|
completeResultWithType
|
||||||
// This property is reserved by the protocol to allow clients and servers to
|
// This property is reserved by the protocol to allow clients and servers to
|
||||||
// attach additional metadata to their responses.
|
// attach additional metadata to their responses.
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
|
|
@ -1306,6 +1329,7 @@ func (x *ListToolsParams) cursorPtr() *string { return &x.Cursor }
|
||||||
|
|
||||||
// The server's response to a tools/list request from the client.
|
// The server's response to a tools/list request from the client.
|
||||||
type ListToolsResult struct {
|
type ListToolsResult struct {
|
||||||
|
completeResultWithType
|
||||||
// This property is reserved by the protocol to allow clients and servers to
|
// This property is reserved by the protocol to allow clients and servers to
|
||||||
// attach additional metadata to their responses.
|
// attach additional metadata to their responses.
|
||||||
Meta `json:"_meta,omitempty"`
|
Meta `json:"_meta,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParamsMeta(t *testing.T) {
|
func TestParamsMeta(t *testing.T) {
|
||||||
|
|
@ -491,7 +492,7 @@ func TestCompleteResult(t *testing.T) {
|
||||||
if err := json.Unmarshal([]byte(test.in), &got); err != nil {
|
if err := json.Unmarshal([]byte(test.in), &got); err != nil {
|
||||||
t.Fatalf("json.Unmarshal(CompleteResult) failed: %v", err)
|
t.Fatalf("json.Unmarshal(CompleteResult) failed: %v", err)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(test.want, got); diff != "" {
|
if diff := cmp.Diff(test.want, got, cmpopts.IgnoreUnexported(CompleteResult{})); diff != "" {
|
||||||
t.Errorf("CompleteResult unmarshal mismatch (-want +got):\n%s", diff)
|
t.Errorf("CompleteResult unmarshal mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1893,7 +1893,14 @@ func (ss *ServerSession) handle(ctx context.Context, req *jsonrpc.Request) (any,
|
||||||
if validatedMeta.usesNewProtocol {
|
if validatedMeta.usesNewProtocol {
|
||||||
ss.setLevel(ctx, &SetLoggingLevelParams{Level: validatedMeta.logLevel})
|
ss.setLevel(ctx, &SetLoggingLevelParams{Level: validatedMeta.logLevel})
|
||||||
}
|
}
|
||||||
return handleReceive(ctx, ss, req)
|
res, err := handleReceive(ctx, ss, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if validatedMeta.usesNewProtocol {
|
||||||
|
setCompleteResultType(res)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitializeParams returns the InitializeParams provided during the client's
|
// InitializeParams returns the InitializeParams provided during the client's
|
||||||
|
|
|
||||||
|
|
@ -1399,6 +1399,148 @@ func TestServerSessionHandle_RejectsInitializeOnNewProtocol(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerSessionHandle_SetsResultTypeOnNewProtocol(t *testing.T) {
|
||||||
|
server := NewServer(testImpl, &ServerOptions{
|
||||||
|
CompletionHandler: func(context.Context, *CompleteRequest) (*CompleteResult, error) {
|
||||||
|
return &CompleteResult{
|
||||||
|
Completion: CompletionResultDetails{
|
||||||
|
Values: []string{"go"},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
AddTool(server, &Tool{Name: "tool"}, func(context.Context, *CallToolRequest, struct{}) (*CallToolResult, any, error) {
|
||||||
|
return &CallToolResult{
|
||||||
|
InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}},
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
server.AddPrompt(&Prompt{Name: "prompt"}, func(context.Context, *GetPromptRequest) (*GetPromptResult, error) {
|
||||||
|
return &GetPromptResult{
|
||||||
|
InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}},
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
server.AddResource(&Resource{URI: "test://resource", Name: "resource"}, func(context.Context, *ReadResourceRequest) (*ReadResourceResult, error) {
|
||||||
|
return &ReadResourceResult{
|
||||||
|
InputRequests: InputRequestMap{"confirm": &ElicitParams{Message: "Continue?"}},
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
newProtocolParams := func(fields map[string]any) map[string]any {
|
||||||
|
params := map[string]any{
|
||||||
|
"_meta": map[string]any{
|
||||||
|
MetaKeyProtocolVersion: protocolVersion20260728,
|
||||||
|
MetaKeyClientInfo: map[string]any{"name": "c", "version": "1"},
|
||||||
|
MetaKeyClientCapabilities: map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for k, v := range fields {
|
||||||
|
params[k] = v
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
params map[string]any
|
||||||
|
want resultType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "discover",
|
||||||
|
method: methodDiscover,
|
||||||
|
params: newProtocolParams(nil),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tools list",
|
||||||
|
method: methodListTools,
|
||||||
|
params: newProtocolParams(nil),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prompts list",
|
||||||
|
method: methodListPrompts,
|
||||||
|
params: newProtocolParams(nil),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resources list",
|
||||||
|
method: methodListResources,
|
||||||
|
params: newProtocolParams(nil),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource templates list",
|
||||||
|
method: methodListResourceTemplates,
|
||||||
|
params: newProtocolParams(nil),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complete",
|
||||||
|
method: methodComplete,
|
||||||
|
params: newProtocolParams(map[string]any{
|
||||||
|
"argument": map[string]any{
|
||||||
|
"name": "language",
|
||||||
|
"value": "g",
|
||||||
|
},
|
||||||
|
"ref": map[string]any{
|
||||||
|
"type": "ref/prompt",
|
||||||
|
"name": "code_review",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
want: resultTypeComplete,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool input required",
|
||||||
|
method: methodCallTool,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "tool", "arguments": map[string]any{}}),
|
||||||
|
want: resultTypeInputRequired,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prompt input required",
|
||||||
|
method: methodGetPrompt,
|
||||||
|
params: newProtocolParams(map[string]any{"name": "prompt"}),
|
||||||
|
want: resultTypeInputRequired,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource input required",
|
||||||
|
method: methodReadResource,
|
||||||
|
params: newProtocolParams(map[string]any{"uri": "test://resource"}),
|
||||||
|
want: resultTypeInputRequired,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ss := &ServerSession{server: server}
|
||||||
|
id, err := jsonrpc.MakeID("test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result, err := ss.handle(context.Background(), &jsonrpc.Request{
|
||||||
|
ID: id,
|
||||||
|
Method: tc.method,
|
||||||
|
Params: mustMarshal(tc.params),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got struct {
|
||||||
|
ResultType string `json:"resultType"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.ResultType != string(tc.want) {
|
||||||
|
t.Fatalf("resultType = %q, want %q; response = %s", got.ResultType, tc.want, data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol verifies that
|
// TestServerSessionHandle_RejectsRemovedMethodsOnNewProtocol verifies that
|
||||||
// the methods removed by SEP-2575 (`initialize`, `notifications/initialized`,
|
// the methods removed by SEP-2575 (`initialize`, `notifications/initialized`,
|
||||||
// `ping`) all return Method not found when the request opts into the new
|
// `ping`) all return Method not found when the request opts into the new
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ supports, its capabilities, and its identity.
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"result": {
|
"result": {
|
||||||
|
"resultType": "complete",
|
||||||
"ttlMs": 0,
|
"ttlMs": 0,
|
||||||
"cacheScope": "public",
|
"cacheScope": "public",
|
||||||
"supportedVersions": [
|
"supportedVersions": [
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ func ExampleLoggingTransport() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// read: {"jsonrpc":"2.0","id":1,"result":{"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","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"}}}
|
||||||
// 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