mcp: add integration for server conformance tests (#1054)
This PR integrates the missing tools to fully support the new protocolVersion on conformance tests.
This commit is contained in:
parent
6de85ec3ed
commit
4c2aece5d7
2 changed files with 558 additions and 20 deletions
6
.github/workflows/conformance.yml
vendored
6
.github/workflows/conformance.yml
vendored
|
|
@ -14,7 +14,7 @@ permissions:
|
|||
contents: read
|
||||
|
||||
env:
|
||||
CONFORMANCE_VERSION: "0.2.0-alpha.5"
|
||||
CONFORMANCE_VERSION: "0.2.0-alpha.9"
|
||||
|
||||
jobs:
|
||||
server-conformance:
|
||||
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
--suite active \
|
||||
--spec-version 2025-11-25 \
|
||||
--expected-failures ./conformance/baseline.yml
|
||||
- name: Run conformance tests (stateless, 2026-07-28)
|
||||
- name: Run conformance tests (stateless, 2026-07-28, full suite)
|
||||
run: |
|
||||
./everything-server -http=":3001" -stateless &
|
||||
SERVER_PID=$!
|
||||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
timeout 30 bash -c 'until curl -s http://localhost:3001/mcp > /dev/null; do sleep 0.5; done'
|
||||
npx -y "@modelcontextprotocol/conformance@${CONFORMANCE_VERSION}" server \
|
||||
--url http://localhost:3001/mcp \
|
||||
--suite active \
|
||||
--suite all \
|
||||
--spec-version 2026-07-28 \
|
||||
--expected-failures ./conformance/baseline.yml
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
)
|
||||
|
|
@ -154,14 +156,19 @@ func registerTools(server *mcp.Server) {
|
|||
Description: "Tests elicitation with enum schema improvements per SEP-1330",
|
||||
}, testElicitationEnumsHandler)
|
||||
|
||||
// SEP-1613 / SEP-2106: JSON Schema 2020-12 conformance test tool.
|
||||
// The scenario verifies that $schema/$defs/additionalProperties (SEP-1613)
|
||||
// and the broader 2020-12 vocabulary — $anchor, allOf/anyOf, if/then/else —
|
||||
// (SEP-2106) survive tools/list verbatim.
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "json_schema_2020_12_tool",
|
||||
Description: "Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)",
|
||||
Description: "Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613, SEP-2106)",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"address": {
|
||||
"$anchor": "addressDef",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": { "type": "string" },
|
||||
|
|
@ -171,8 +178,20 @@ func registerTools(server *mcp.Server) {
|
|||
},
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"address": { "$ref": "#/$defs/address" }
|
||||
"address": { "$ref": "#/$defs/address" },
|
||||
"contactMethod": { "type": "string", "enum": ["phone", "email"] },
|
||||
"phone": { "type": "string" },
|
||||
"email": { "type": "string" }
|
||||
},
|
||||
"allOf": [
|
||||
{ "anyOf": [{ "required": ["phone"] }, { "required": ["email"] }] }
|
||||
],
|
||||
"if": {
|
||||
"properties": { "contactMethod": { "const": "phone" } },
|
||||
"required": ["contactMethod"]
|
||||
},
|
||||
"then": { "required": ["phone"] },
|
||||
"else": { "required": ["email"] },
|
||||
"additionalProperties": false
|
||||
}`),
|
||||
}, jsonSchema202012Handler)
|
||||
|
|
@ -181,6 +200,87 @@ func registerTools(server *mcp.Server) {
|
|||
Name: "test_reconnection",
|
||||
Description: "Tests SSE stream disconnection and client reconnection (SEP-1699). Server will close the stream mid-call and send the result after client reconnects.",
|
||||
}, testReconnectionHandler)
|
||||
|
||||
// SEP-2243 x-mcp-header tool — arms the http-custom-header-server-validation
|
||||
// conformance scenario (which skips when no tool with an x-mcp-header
|
||||
// annotation is found).
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_x_mcp_header",
|
||||
Description: "Tests SEP-2243 Mcp-Param-* server-side validation",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"region": { "type": "string", "description": "mirrored into Mcp-Param-Region", "x-mcp-header": "Region" },
|
||||
"level": { "type": "integer", "description": "non-mirrored argument" }
|
||||
}
|
||||
}`),
|
||||
}, testXMcpHeaderHandler)
|
||||
|
||||
// SEP-2575 diagnostic tools used by the stateless conformance scenario.
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_missing_capability",
|
||||
Description: "Requires the sampling client capability; used to verify MissingRequiredClientCapabilityError (-32021) (SEP-2575)",
|
||||
}, testMissingCapabilityHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_streaming_elicitation",
|
||||
Description: "Streams progress notifications while a call is in flight; used to verify response streams carry only notifications, never independent JSON-RPC requests (SEP-2575)",
|
||||
}, testStreamingElicitationHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_logging_tool",
|
||||
Description: "Attempts to emit a log message; the framework must drop it when the client did not set _meta.../logLevel (SEP-2575)",
|
||||
}, testLoggingToolHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_trigger_tool_change",
|
||||
Description: "Mutates the tool list to trigger a notifications/tools/list_changed on active subscription streams (SEP-2575)",
|
||||
}, makeTriggerToolChangeHandler(server))
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_trigger_prompt_change",
|
||||
Description: "Mutates the prompt list to trigger a notifications/prompts/list_changed on active subscription streams (SEP-2575)",
|
||||
}, makeTriggerPromptChangeHandler(server))
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_elicitation",
|
||||
Description: "MRTR (SEP-2322): asks for the caller name via an in-band elicitation request",
|
||||
}, testInputRequiredElicitationHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_sampling",
|
||||
Description: "MRTR (SEP-2322): asks for an LLM completion via an in-band sampling request",
|
||||
}, testInputRequiredSamplingHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_list_roots",
|
||||
Description: "MRTR (SEP-2322): asks for the client roots via an in-band roots/list request",
|
||||
}, testInputRequiredListRootsHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_request_state",
|
||||
Description: "MRTR (SEP-2322): round-trips integrity-protected requestState alongside an elicitation request",
|
||||
}, testInputRequiredRequestStateHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_multiple_inputs",
|
||||
Description: "MRTR (SEP-2322): asks for elicitation, sampling and roots input in a single round",
|
||||
}, testInputRequiredMultipleInputsHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_multi_round",
|
||||
Description: "MRTR (SEP-2322): two elicitation rounds with evolving requestState before completing",
|
||||
}, testInputRequiredMultiRoundHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_tampered_state",
|
||||
Description: "MRTR (SEP-2322): rejects retries whose requestState fails integrity verification",
|
||||
}, testInputRequiredTamperedStateHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_input_required_result_capabilities",
|
||||
Description: "MRTR (SEP-2322): only requests input kinds the declared client capabilities cover",
|
||||
}, testInputRequiredCapabilitiesHandler)
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
|
|
@ -477,33 +577,466 @@ func testElicitationEnumsHandler(ctx context.Context, req *mcp.CallToolRequest,
|
|||
}, nil, nil
|
||||
}
|
||||
|
||||
type jsonSchemaAddress struct {
|
||||
Street string `json:"street"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
type jsonSchemaInput struct {
|
||||
Name string `json:"name"`
|
||||
Address *jsonSchemaAddress `json:"address"`
|
||||
}
|
||||
|
||||
func jsonSchema202012Handler(ctx context.Context, req *mcp.CallToolRequest, input jsonSchemaInput) (*mcp.CallToolResult, any, error) {
|
||||
// Echo back the arguments received
|
||||
var addressStr string
|
||||
if input.Address != nil {
|
||||
addressStr = fmt.Sprintf("{street: %q, city: %q}", input.Address.Street, input.Address.City)
|
||||
} else {
|
||||
addressStr = "nil"
|
||||
}
|
||||
func jsonSchema202012Handler(ctx context.Context, req *mcp.CallToolRequest, input json.RawMessage) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{
|
||||
Text: fmt.Sprintf("Received: name=%q, address=%s", input.Name, addressStr),
|
||||
Text: fmt.Sprintf("JSON Schema 2020-12 tool called with: %s", input),
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
type testXMcpHeaderInput struct {
|
||||
Region string `json:"region,omitempty"`
|
||||
Level int `json:"level,omitempty"`
|
||||
}
|
||||
|
||||
func testXMcpHeaderHandler(ctx context.Context, req *mcp.CallToolRequest, input testXMcpHeaderInput) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("region=%s", input.Region)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testMissingCapabilityHandler requires the client to have declared the
|
||||
// sampling capability. When absent, it returns a MissingRequiredClientCapabilityError
|
||||
// with data.requiredCapabilities = {"sampling": {}} per SEP-2575.
|
||||
func testMissingCapabilityHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
caps := req.ClientCapabilities()
|
||||
if caps != nil && caps.Sampling != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Client declared the sampling capability; tool executed."},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
missingCapabilityData := mcp.MissingRequiredClientCapabilityData{
|
||||
RequiredCapabilities: &mcp.ClientCapabilities{
|
||||
Sampling: &mcp.SamplingCapabilities{},
|
||||
},
|
||||
}
|
||||
dataBytes, err := json.Marshal(missingCapabilityData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, &jsonrpc.Error{
|
||||
Code: mcp.CodeMissingRequiredClientCapabilities,
|
||||
Message: "sampling capability required but not declared by client",
|
||||
Data: dataBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// testStreamingElicitationHandler returns a plain result. Per SEP-2575, the
|
||||
// response stream must carry no independent top-level JSON-RPC requests; a
|
||||
// plain response trivially satisfies this. The scenario declares no
|
||||
// `elicitation` capability, so this handler must not call req.Session.Elicit.
|
||||
func testStreamingElicitationHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "stream observed: result frames only, no top-level requests"},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testLoggingToolHandler attempts to emit a log message. The SDK's
|
||||
// ServerSession.Log gates on the per-request _meta.../logLevel, so this is a
|
||||
// no-op notification-wise when the client did not opt in.
|
||||
func testLoggingToolHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
req.Session.Log(ctx, &mcp.LoggingMessageParams{
|
||||
Level: "info",
|
||||
Data: "test_logging_tool executed",
|
||||
})
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Log attempted; framework gates on _meta.../logLevel."},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// makeTriggerToolChangeHandler returns a handler that (re-)registers a
|
||||
// no-op transient tool. Every call to Server.AddTool dispatches a
|
||||
// notifications/tools/list_changed to active subscription streams (even when
|
||||
// it replaces a tool with the same name), which is what the SEP-2575
|
||||
// subscription checks assert.
|
||||
func makeTriggerToolChangeHandler(server *mcp.Server) mcp.ToolHandlerFor[any, any] {
|
||||
return func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "__transient_tool_for_list_changed",
|
||||
Description: "Transient tool used to trigger tools/list_changed",
|
||||
}, func(context.Context, *mcp.CallToolRequest, any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{}, nil, nil
|
||||
})
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "tools_list_changed published"},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// makeTriggerPromptChangeHandler is the prompt analogue of
|
||||
// makeTriggerToolChangeHandler.
|
||||
func makeTriggerPromptChangeHandler(server *mcp.Server) mcp.ToolHandlerFor[any, any] {
|
||||
return func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
server.AddPrompt(&mcp.Prompt{
|
||||
Name: "__transient_prompt_for_list_changed",
|
||||
Description: "Transient prompt used to trigger prompts/list_changed",
|
||||
}, func(context.Context, *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{}, nil
|
||||
})
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "prompts_list_changed published"},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEP-2322 Multi-Round-Trip helpers
|
||||
// =============================================================================
|
||||
|
||||
// requestStateTamperedSuffix is what the tampered-state conformance scenario
|
||||
// appends to a valid requestState. Detecting this suffix is sufficient to
|
||||
// pass the check; a production server would use a real integrity mechanism
|
||||
// (HMAC / AEAD, per SEP-2322 § "Server Requirements").
|
||||
const requestStateTamperedSuffix = "-TAMPERED"
|
||||
|
||||
// requestStateValid reports whether the state looks like one this server
|
||||
// minted (i.e. the harness did not append the tampered marker).
|
||||
func requestStateValid(state string) bool {
|
||||
return state != "" && !strings.HasSuffix(state, requestStateTamperedSuffix)
|
||||
}
|
||||
|
||||
// acceptedContent returns the "content" field of an ElicitResult only when the
|
||||
// user accepted the elicitation. Returns nil if the response is missing, of the
|
||||
// wrong type, or the action is not "accept".
|
||||
func acceptedContent(responses mcp.InputResponseMap, key string) map[string]any {
|
||||
if responses == nil {
|
||||
return nil
|
||||
}
|
||||
resp, ok := responses[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
elicit, ok := resp.(*mcp.ElicitResult)
|
||||
if !ok || elicit == nil || elicit.Action != "accept" {
|
||||
return nil
|
||||
}
|
||||
return elicit.Content
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEP-2322 Multi-Round-Trip tool handlers
|
||||
// =============================================================================
|
||||
|
||||
// nameElicitSchema is the requestedSchema used by tools that elicit a name.
|
||||
func nameElicitSchema() *jsonschema.Schema {
|
||||
return &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"name": {Type: "string"},
|
||||
},
|
||||
Required: []string{"name"},
|
||||
}
|
||||
}
|
||||
|
||||
// testInputRequiredElicitationHandler asks for the caller name via elicitation.
|
||||
// Reused by the result-type, missing-input-response, ignore-extra-params, and
|
||||
// validate-input scenarios: anything that does not carry an accepted
|
||||
// "user_name" response is answered with a fresh InputRequiredResult.
|
||||
func testInputRequiredElicitationHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
if content := acceptedContent(req.Params.InputResponses, "user_name"); content != nil {
|
||||
name, _ := content["name"].(string)
|
||||
if name != "" {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("Hello, %s!", name)}},
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"user_name": &mcp.ElicitParams{
|
||||
Message: "What is your name?",
|
||||
RequestedSchema: nameElicitSchema(),
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// samplingResponseText extracts the first text content from a sampling
|
||||
// response. The Go SDK unmarshals responses with a "role" field into
|
||||
// *CreateMessageWithToolsResult (a superset of the legacy single-content
|
||||
// CreateMessageResult).
|
||||
func samplingResponseText(responses mcp.InputResponseMap, key string) (string, bool) {
|
||||
resp, ok := responses[key].(*mcp.CreateMessageWithToolsResult)
|
||||
if !ok || resp == nil {
|
||||
return "", false
|
||||
}
|
||||
for _, c := range resp.Content {
|
||||
if tc, ok := c.(*mcp.TextContent); ok {
|
||||
return tc.Text, true
|
||||
}
|
||||
}
|
||||
return "(non-text response)", true
|
||||
}
|
||||
|
||||
// testInputRequiredSamplingHandler asks for an LLM completion via sampling.
|
||||
func testInputRequiredSamplingHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
if text, ok := samplingResponseText(req.Params.InputResponses, "capital_question"); ok {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("Sampling response: %s", text)}},
|
||||
}, nil, nil
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"capital_question": &mcp.CreateMessageParams{
|
||||
Messages: []*mcp.SamplingMessage{
|
||||
{Role: "user", Content: &mcp.TextContent{Text: "What is the capital of France?"}},
|
||||
},
|
||||
MaxTokens: 100,
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testInputRequiredListRootsHandler asks for the client roots.
|
||||
func testInputRequiredListRootsHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
if resp, ok := req.Params.InputResponses["client_roots"].(*mcp.ListRootsResult); ok && resp != nil {
|
||||
var uris []string
|
||||
for _, r := range resp.Roots {
|
||||
uris = append(uris, r.URI)
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Client exposed %d root(s): %s", len(resp.Roots), strings.Join(uris, ", "))},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"client_roots": &mcp.ListRootsParams{},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testInputRequiredRequestStateHandler round-trips an opaque requestState
|
||||
// alongside an elicitation request.
|
||||
func testInputRequiredRequestStateHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
if content := acceptedContent(req.Params.InputResponses, "confirm"); content != nil {
|
||||
if requestStateValid(req.Params.RequestState) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "state-ok: requestState received and confirmation accepted"}},
|
||||
}, nil, nil
|
||||
}
|
||||
return nil, nil, &jsonrpc.Error{
|
||||
Code: jsonrpc.CodeInvalidParams,
|
||||
Message: "invalid requestState",
|
||||
}
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"confirm": &mcp.ElicitParams{
|
||||
Message: "Please confirm",
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{"ok": {Type: "boolean"}},
|
||||
Required: []string{"ok"},
|
||||
},
|
||||
},
|
||||
},
|
||||
RequestState: "request_state",
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testInputRequiredMultipleInputsHandler asks for elicitation, sampling, and
|
||||
// roots input in a single round.
|
||||
func testInputRequiredMultipleInputsHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
nameContent := acceptedContent(req.Params.InputResponses, "user_name")
|
||||
name, _ := nameContent["name"].(string)
|
||||
|
||||
greetingText, hasGreeting := samplingResponseText(req.Params.InputResponses, "greeting")
|
||||
|
||||
var rootsCount int
|
||||
rootsResp, rootsOK := req.Params.InputResponses["client_roots"].(*mcp.ListRootsResult)
|
||||
if rootsOK && rootsResp != nil {
|
||||
rootsCount = len(rootsResp.Roots)
|
||||
}
|
||||
|
||||
if name != "" && hasGreeting && rootsOK {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("%s %s — %d root(s) visible", greetingText, name, rootsCount)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"user_name": &mcp.ElicitParams{
|
||||
Message: "What is your name?",
|
||||
RequestedSchema: nameElicitSchema(),
|
||||
},
|
||||
"greeting": &mcp.CreateMessageParams{
|
||||
Messages: []*mcp.SamplingMessage{
|
||||
{Role: "user", Content: &mcp.TextContent{Text: "Generate a greeting"}},
|
||||
},
|
||||
MaxTokens: 50,
|
||||
},
|
||||
"client_roots": &mcp.ListRootsParams{},
|
||||
},
|
||||
RequestState: "multiple_inputs",
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testInputRequiredMultiRoundHandler runs two elicitation rounds with
|
||||
// evolving requestState before completing. The round number (and the name
|
||||
// elicited in round 1) lives in the state, since the stateless server keeps
|
||||
// no per-session memory.
|
||||
func testInputRequiredMultiRoundHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
state := req.Params.RequestState
|
||||
// State format: "round=1" or "round=2;name=<name>".
|
||||
switch {
|
||||
case state == "":
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"step1": &mcp.ElicitParams{
|
||||
Message: "Step 1: What is your name?",
|
||||
RequestedSchema: nameElicitSchema(),
|
||||
},
|
||||
},
|
||||
RequestState: "round=1",
|
||||
}, nil, nil
|
||||
case state == "round=1":
|
||||
name, _ := acceptedContent(req.Params.InputResponses, "step1")["name"].(string)
|
||||
if name == "" {
|
||||
name = "unknown"
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"step2": &mcp.ElicitParams{
|
||||
Message: "Step 2: What is your favorite color?",
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{"color": {Type: "string"}},
|
||||
Required: []string{"color"},
|
||||
},
|
||||
},
|
||||
},
|
||||
RequestState: "round=2;name=" + name,
|
||||
}, nil, nil
|
||||
default:
|
||||
color, _ := acceptedContent(req.Params.InputResponses, "step2")["color"].(string)
|
||||
if color == "" {
|
||||
color = "unknown"
|
||||
}
|
||||
name := strings.TrimPrefix(state, "round=2;name=")
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Multi-round complete: %s likes %s", name, color)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// testInputRequiredTamperedStateHandler rejects retries whose requestState
|
||||
// looks tampered. The conformance harness tampers by appending "-TAMPERED";
|
||||
// production servers should use a real integrity mechanism (HMAC / AEAD).
|
||||
func testInputRequiredTamperedStateHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
if req.Params.RequestState != "" {
|
||||
if !requestStateValid(req.Params.RequestState) {
|
||||
return nil, nil, &jsonrpc.Error{
|
||||
Code: jsonrpc.CodeInvalidParams,
|
||||
Message: "invalid requestState: tampered",
|
||||
}
|
||||
}
|
||||
if acceptedContent(req.Params.InputResponses, "confirm") != nil {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "state-ok: requestState received and confirmation accepted"}},
|
||||
}, nil, nil
|
||||
}
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"confirm": &mcp.ElicitParams{
|
||||
Message: "Please confirm",
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{"ok": {Type: "boolean"}},
|
||||
Required: []string{"ok"},
|
||||
},
|
||||
},
|
||||
},
|
||||
RequestState: "tampered_state",
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// testInputRequiredCapabilitiesHandler only requests input kinds the client
|
||||
// declared capabilities cover. Reads capabilities from the per-request
|
||||
// _meta["io.modelcontextprotocol/clientCapabilities"] envelope.
|
||||
func testInputRequiredCapabilitiesHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
if len(req.Params.InputResponses) > 0 {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "Capability-aware input requests fulfilled"}},
|
||||
}, nil, nil
|
||||
}
|
||||
caps := req.ClientCapabilities()
|
||||
reqs := mcp.InputRequestMap{}
|
||||
if caps != nil && caps.Elicitation != nil {
|
||||
reqs["user_name"] = &mcp.ElicitParams{
|
||||
Message: "What is your name?",
|
||||
RequestedSchema: nameElicitSchema(),
|
||||
}
|
||||
}
|
||||
if caps != nil && caps.Sampling != nil {
|
||||
reqs["greeting"] = &mcp.CreateMessageParams{
|
||||
Messages: []*mcp.SamplingMessage{
|
||||
{Role: "user", Content: &mcp.TextContent{Text: "Generate a short greeting"}},
|
||||
},
|
||||
MaxTokens: 50,
|
||||
}
|
||||
}
|
||||
if caps != nil && (caps.RootsV2 != nil || caps.Roots.ListChanged) {
|
||||
reqs["client_roots"] = &mcp.ListRootsParams{}
|
||||
}
|
||||
if len(reqs) == 0 {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{&mcp.TextContent{Text: "No declared client capability supports an in-band input request"}},
|
||||
}, nil, nil
|
||||
}
|
||||
return &mcp.CallToolResult{InputRequests: reqs}, nil, nil
|
||||
}
|
||||
|
||||
// testInputRequiredPromptHandler is the prompts/get analogue: elicits context
|
||||
// on the first call and returns a GetPromptResult with messages on retry.
|
||||
func testInputRequiredPromptHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
if content := acceptedContent(req.Params.InputResponses, "user_context"); content != nil {
|
||||
contextStr, _ := content["context"].(string)
|
||||
return &mcp.GetPromptResult{
|
||||
Description: "A prompt with elicited context",
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.TextContent{Text: fmt.Sprintf("Context: %s", contextStr)},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return &mcp.GetPromptResult{
|
||||
InputRequests: mcp.InputRequestMap{
|
||||
"user_context": &mcp.ElicitParams{
|
||||
Message: "Please provide context for the prompt",
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{"context": {Type: "string"}},
|
||||
Required: []string{"context"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func testReconnectionHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
// Close the SSE stream to trigger client reconnection (SEP-1699)
|
||||
if req.Extra != nil && req.Extra.CloseSSEStream != nil {
|
||||
|
|
@ -651,6 +1184,11 @@ func registerPrompts(server *mcp.Server) {
|
|||
Title: "Prompt With Image",
|
||||
Description: "A prompt that includes image content",
|
||||
}, promptWithImageHandler)
|
||||
|
||||
server.AddPrompt(&mcp.Prompt{
|
||||
Name: "test_input_required_result_prompt",
|
||||
Description: "MRTR (SEP-2322): asks for context via an in-band elicitation request",
|
||||
}, testInputRequiredPromptHandler)
|
||||
}
|
||||
|
||||
func simplePromptHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue