Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions pkgs/mcpx/docs/spec/conformance-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Generated by `internal/conformance` (`MCPX_UPDATE_MATRIX=1 go test ./internal/conformance -run TestMatrix`); do not edit.

Generated from inputs `819a4c9fcb265a84` (sha256 of docs/spec/requirements.md, internal/conformance/overrides/*.json, internal/conformance/coverage*.go). `TestMatrix` regenerates this file and fails when it differs, so it describes the tree it is committed in.
Generated from inputs `d87c25241c253b00` (sha256 of docs/spec/requirements.md, internal/conformance/overrides/*.json, internal/conformance/coverage*.go). `TestMatrix` regenerates this file and fails when it differs, so it describes the tree it is committed in.

Every normative requirement of every revision (catalogue: `docs/spec/requirements.md`), for mcpx as a server and as a client. A requirement is *tested* when a named test verifies it in that revision, *n/a* when it does not bind mcpx in that role, and a *gap* when it binds mcpx and mcpx does not meet it yet, with the issue that tracks it. Run exactly the covering tests with `go run ./internal/conformance/cmd/covering`.

Expand Down Expand Up @@ -36,37 +36,37 @@ Every normative requirement of every revision (catalogue: `docs/spec/requirement
| 2025-03-26 | client | other | 7 | 7 | 0 | 0 | 5 |
| 2025-06-18 | server | MUST | 65 | 50 | 15 | 0 | 36 |
| 2025-06-18 | server | MUST NOT | 10 | 9 | 1 | 0 | 8 |
| 2025-06-18 | server | SHOULD | 52 | 45 | 7 | 0 | 63 |
| 2025-06-18 | server | SHOULD | 52 | 46 | 6 | 0 | 63 |
| 2025-06-18 | server | SHOULD NOT | 4 | 3 | 1 | 0 | 1 |
| 2025-06-18 | server | MAY | 17 | 15 | 2 | 0 | 18 |
| 2025-06-18 | server | other | 15 | 15 | 0 | 0 | 3 |
| 2025-06-18 | client | MUST | 52 | 36 | 16 | 0 | 49 |
| 2025-06-18 | client | MUST NOT | 13 | 11 | 2 | 0 | 5 |
| 2025-06-18 | client | SHOULD | 52 | 39 | 13 | 0 | 63 |
| 2025-06-18 | client | SHOULD | 52 | 40 | 12 | 0 | 63 |
| 2025-06-18 | client | SHOULD NOT | 2 | 1 | 1 | 0 | 3 |
| 2025-06-18 | client | MAY | 15 | 13 | 2 | 0 | 20 |
| 2025-06-18 | client | other | 10 | 8 | 2 | 0 | 8 |
| 2025-11-25 | server | MUST | 115 | 97 | 18 | 0 | 65 |
| 2025-11-25 | server | MUST NOT | 22 | 19 | 3 | 0 | 13 |
| 2025-11-25 | server | SHOULD | 78 | 62 | 16 | 0 | 81 |
| 2025-11-25 | server | SHOULD | 78 | 63 | 15 | 0 | 81 |
| 2025-11-25 | server | SHOULD NOT | 11 | 10 | 1 | 0 | 3 |
| 2025-11-25 | server | MAY | 27 | 25 | 2 | 0 | 28 |
| 2025-11-25 | server | other | 19 | 19 | 0 | 0 | 2 |
| 2025-11-25 | client | MUST | 76 | 56 | 20 | 0 | 104 |
| 2025-11-25 | client | MUST NOT | 16 | 14 | 2 | 0 | 19 |
| 2025-11-25 | client | SHOULD | 67 | 46 | 21 | 0 | 92 |
| 2025-11-25 | client | SHOULD | 67 | 47 | 20 | 0 | 92 |
| 2025-11-25 | client | SHOULD NOT | 5 | 4 | 1 | 0 | 9 |
| 2025-11-25 | client | MAY | 20 | 17 | 3 | 0 | 35 |
| 2025-11-25 | client | other | 13 | 11 | 2 | 0 | 8 |
| 2026-07-28 | server | MUST | 128 | 108 | 20 | 0 | 96 |
| 2026-07-28 | server | MUST NOT | 44 | 42 | 2 | 0 | 20 |
| 2026-07-28 | server | SHOULD | 74 | 61 | 13 | 0 | 86 |
| 2026-07-28 | server | SHOULD | 74 | 62 | 12 | 0 | 86 |
| 2026-07-28 | server | SHOULD NOT | 11 | 10 | 1 | 0 | 9 |
| 2026-07-28 | server | MAY | 31 | 29 | 2 | 0 | 30 |
| 2026-07-28 | server | other | 23 | 22 | 1 | 0 | 4 |
| 2026-07-28 | client | MUST | 91 | 64 | 27 | 0 | 133 |
| 2026-07-28 | client | MUST NOT | 25 | 23 | 2 | 0 | 39 |
| 2026-07-28 | client | SHOULD | 74 | 54 | 20 | 0 | 86 |
| 2026-07-28 | client | SHOULD | 74 | 55 | 19 | 0 | 86 |
| 2026-07-28 | client | SHOULD NOT | 13 | 11 | 2 | 0 | 7 |
| 2026-07-28 | client | MAY | 14 | 11 | 3 | 0 | 47 |
| 2026-07-28 | client | other | 13 | 11 | 2 | 0 | 14 |
Expand Down Expand Up @@ -862,7 +862,7 @@ Every normative requirement of every revision (catalogue: `docs/spec/requirement
| [elicitation-client-allow-decline-any-time](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28` |
| [elicitation-client-rate-limit](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | **gap** #257: no per-server limit on elicitation requests |
| [elicitation-client-clear-what-and-why](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `conformance.TestClientFeaturesClient/2025-06-18/elicitation/elicitation-client-clear-what-and-why` |
| [elicitation-both-validate-against-schema](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#security-considerations) | SHOULD | both | **gap** #255: an answer is checked only for non-empty content, not against requestedSchema | **gap** #255: an answer is checked only for non-empty content, not against requestedSchema |
| [elicitation-both-validate-against-schema](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#security-considerations) | SHOULD | both | tested: `mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema@2025-06-18` | tested: `mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema@2025-06-18` |

### errors

Expand Down Expand Up @@ -1341,8 +1341,8 @@ Every normative requirement of every revision (catalogue: `docs/spec/requirement
| [elicitation-client-allow-decline-any-time](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28` |
| [elicitation-client-rate-limit](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | **gap** #257: no per-server limit on elicitation requests |
| [elicitation-client-clear-what-and-why](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `conformance.TestClientFeaturesClient/2025-11-25/elicitation/elicitation-client-clear-what-and-why` |
| [elicitation-client-validate-responses](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | client | n/a: addresses the other side | **gap** #255: an answer is checked only for non-empty content, not against requestedSchema |
| [elicitation-server-validate-received-data](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | server | **gap** #255: a client answer is relayed upstream without a schema check | n/a: addresses the other side |
| [elicitation-client-validate-responses](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | client | n/a: addresses the other side | tested: `mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2025-11-25/elicitation/wrong-typed-answer-is-32602-not-relayed` |
| [elicitation-server-validate-received-data](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | server | tested: `mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2025-11-25/elicitation/wrong-typed-answer-is-32602-not-relayed` | n/a: addresses the other side |
| [elicitation-url-no-sensitive-user-info](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handling) | MUST NOT | server | tested: `conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-no-sensitive-user-info` | n/a: addresses the other side |
| [elicitation-url-not-pre-authenticated](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handling) | MUST NOT | server | tested: `conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-url-not-pre-authenticated` | n/a: addresses the other side |
| [elicitation-no-clickable-urls-in-form](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handling) | SHOULD NOT | server | tested: `conformance.TestClientFeaturesServer/2025-11-25/elicitation/elicitation-no-clickable-urls-in-form` | n/a: addresses the other side |
Expand Down Expand Up @@ -2011,8 +2011,8 @@ Every normative requirement of every revision (catalogue: `docs/spec/requirement
| [elicitation-client-user-approval-controls](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `elicit.TestRoutingSendsCredentialsToAHumanAndTheRestToTheAgent@2025-06-18,2025-11-25,2026-07-28` |
| [elicitation-client-allow-decline-any-time](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `elicit.TestAnsweringAfterTheProcessDiedStillWorks@2025-06-18,2025-11-25,2026-07-28` |
| [elicitation-client-clear-what-and-why](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#security-considerations) | SHOULD | client | n/a: addresses the other side | tested: `conformance.TestClientFeaturesClient/2026-07-28/elicitation/elicitation-client-clear-what-and-why` |
| [elicitation-client-validate-responses](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | client | n/a: addresses the other side | **gap** #255: an answer is checked only for non-empty content, not against requestedSchema |
| [elicitation-server-validate-received-data](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | server | **gap** #255: a client answer is relayed upstream without a schema check | n/a: addresses the other side |
| [elicitation-client-validate-responses](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | client | n/a: addresses the other side | tested: `mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2026-07-28/elicitation/input-required-wrong-typed-answer-is-rejected` |
| [elicitation-server-validate-received-data](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#form-mode-security) | SHOULD | server | tested: `mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2026-07-28/elicitation/input-required-wrong-typed-answer-is-rejected` | n/a: addresses the other side |
| [elicitation-url-no-sensitive-user-info](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handling) | MUST NOT | server | tested: `conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-no-sensitive-user-info` | n/a: addresses the other side |
| [elicitation-url-not-pre-authenticated](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handling) | MUST NOT | server | tested: `conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-url-not-pre-authenticated` | n/a: addresses the other side |
| [elicitation-no-clickable-urls-in-form](https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#safe-url-handling) | SHOULD NOT | server | tested: `conformance.TestClientFeaturesServer/2026-07-28/elicitation/elicitation-no-clickable-urls-in-form` | n/a: addresses the other side |
Expand Down
10 changes: 6 additions & 4 deletions pkgs/mcpx/internal/conformance/coverage_clientfeatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ func init() {
const storeProtected = "elicit.TestOpenAtCreatesOwnerOnlyStore@2025-11-25,2026-07-28"
const storeMigrated = "elicit.TestOpenAtMigratesLegacyStore@2025-11-25,2026-07-28"
register(
C("elicitation-client-validate-responses", "mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2025-11-25/elicitation/wrong-typed-answer-is-32602-not-relayed",
"mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2026-07-28/elicitation/input-required-wrong-typed-answer-is-rejected"),
C("elicitation-both-validate-against-schema", "mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema@2025-06-18"),
S("elicitation-both-validate-against-schema", "mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema@2025-06-18"),
S("elicitation-server-validate-received-data", "mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2025-11-25/elicitation/wrong-typed-answer-is-32602-not-relayed",
"mcpclient.TestElicitationAnswersValidatedAgainstRequestedSchema/2026-07-28/elicitation/input-required-wrong-typed-answer-is-rejected"),
C("elicitation-client-32602-on-undeclared-mode", "mcpclient.TestElicitationDeclarations/2025-11-25/elicitation/undeclared-mode-is-32602"),
C("sampling-client-error-on-tools-without-capability", "mcpclient.TestSamplingDeclarations/2025-11-25/sampling/tools-without-capability-is-32602",
"mcpclient.TestModernSamplingShape/2026-07-28/sampling/tools-without-capability-is-32602"),
Expand Down Expand Up @@ -130,10 +136,6 @@ func init() {
Gap{ID: "elicitation-client-handle-urls-carefully", Side: Client, Issue: "#255", Why: "mcpx elicit prints the raw request: no review step, no domain emphasis or punycode warning, no defaults pre-filled"},
Gap{ID: "elicitation-client-highlight-domain", Side: Client, Issue: "#255", Why: "mcpx elicit prints the raw request: no review step, no domain emphasis or punycode warning, no defaults pre-filled"},
Gap{ID: "elicitation-client-warn-punycode", Side: Client, Issue: "#255", Why: "mcpx elicit prints the raw request: no review step, no domain emphasis or punycode warning, no defaults pre-filled"},
Gap{ID: "elicitation-client-validate-responses", Side: Client, Issue: "#255", Why: "an answer is checked only for non-empty content, not against requestedSchema"},
Gap{ID: "elicitation-both-validate-against-schema", Side: Client, Issue: "#255", Why: "an answer is checked only for non-empty content, not against requestedSchema"},
Gap{ID: "elicitation-both-validate-against-schema", Side: Server, Issue: "#255", Why: "an answer is checked only for non-empty content, not against requestedSchema"},
Gap{ID: "elicitation-server-validate-received-data", Side: Server, Issue: "#255", Why: "a client answer is relayed upstream without a schema check"},
Gap{ID: "elicitation-server-must-return-32042", Side: Server, Issue: "#214", Why: "a url question for a client that cannot take it stays with the broker; -32042 is never returned"},
Gap{ID: "elicitation-server-state-bound-to-user", Side: Server, Issue: "#257", Why: "mcpx has no authenticated user: state is bound to the request, the session id is whatever the caller sends"},
Gap{ID: "elicitation-server-stored-state-bound-to-user", Side: Server, Issue: "#257", Why: "mcpx has no authenticated user: state is bound to the request, the session id is whatever the caller sends"},
Expand Down
73 changes: 73 additions & 0 deletions pkgs/mcpx/internal/mcpclient/elicitation_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package mcpclient

import (
"encoding/json"
"fmt"

"github.com/dezren39/mcpx/internal/schemaval"
)

// This file wires internal/schemaval into elicitation answers (Phase 3
// of #1481, issue #1484). Client.answer is the single choke point every
// elicitation answer passes through — a server's own elicitation/create
// request and a 2026-07-28 input_required round alike — so the check
// lives there, after applyDefaults has filled omitted fields from the
// schema's defaults: an omitted field is valid only once its default
// is in.

// checkElicitationAnswer validates an accepted form answer's content
// against the requestedSchema the requesting server itself supplied.
//
// - A validation failure is Invalid Params (-32602) to the requesting
// server, naming the failing paths, plus a Warning; the answer is
// NOT relayed — the unvalidated answer never reaches the upstream.
// - A requestedSchema that does not compile is the server's own
// defect, not the answerer's: warn and relay the answer
// unvalidated rather than wedging the user's answer on a schema
// they never wrote.
// - Decline/cancel answers (no content to validate), URL mode, and
// requests without a requestedSchema pass through untouched.
func (c *Client) checkElicitationAnswer(out any, params json.RawMessage) (any, *rpcError) {
var p struct {
Mode string `json:"mode"`
RequestedSchema json.RawMessage `json:"requestedSchema"`
}
if json.Unmarshal(params, &p) != nil || len(p.RequestedSchema) == 0 {
return out, nil
}
// Undeclared mode is form (the codebase already treats it so, and
// elicitModeDeclared has already refused any other undeclared
// mode); a declared non-form mode — URL — carries no form content
// to validate.
if p.Mode != "" && p.Mode != "form" {
return out, nil
}
b, err := json.Marshal(out)
if err != nil {
return out, nil
}
var res map[string]json.RawMessage
if json.Unmarshal(b, &res) != nil {
return out, nil
}
var action string
_ = json.Unmarshal(res["action"], &action)
if action != "accept" {
return out, nil
}
content, ok := res["content"]
if !ok || string(content) == "null" {
return out, nil
}
schema, err := schemaval.Compile(p.RequestedSchema)
if err != nil {
c.warn(Warning{Reason: fmt.Sprintf("elicitation answer relayed unvalidated: requestedSchema does not compile: %v", err)})
return out, nil
}
if err := schema.ValidateJSON(content); err != nil {
c.warn(Warning{Reason: fmt.Sprintf("elicitation answer rejected: content does not match requestedSchema: %v", err)})
return nil, &rpcError{Code: codeInvalidParams,
Message: fmt.Sprintf("elicitation answer does not match requestedSchema: %v", err)}
}
return out, nil
}
Loading
Loading