Skip to content
Open
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
5 changes: 5 additions & 0 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,11 @@ defaulted field `Required` defeats it: accepted content is validated against
the schema before defaults are applied, so an answer that omits the field is
rejected rather than defaulted.

An elicitation handler that accepts the form as offered can return
`&ElicitResult{Action: "accept"}` with no `Content`. The client treats that
as an empty answer: every field takes its default, and a required field
without one is rejected.

An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330-elicitation-enum-schema-improvements-and-standards)) restricts a
field to a fixed set of values, which the client renders as a choice. Enums
are supported only on `"string"` fields; declaring one on another type is
Expand Down
5 changes: 5 additions & 0 deletions internal/docs/client.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ defaulted field `Required` defeats it: accepted content is validated against
the schema before defaults are applied, so an answer that omits the field is
rejected rather than defaulted.

An elicitation handler that accepts the form as offered can return
`&ElicitResult{Action: "accept"}` with no `Content`. The client treats that
as an empty answer: every field takes its default, and a required field
without one is rejected.

An `Enum` ([SEP-1330](https://modelcontextprotocol.io/seps/1330-elicitation-enum-schema-improvements-and-standards)) restricts a
field to a fixed set of values, which the client renders as a choice. Enums
are supported only on `"string"` fields; declaring one on another type is
Expand Down
7 changes: 6 additions & 1 deletion mcp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,13 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult,
if res == nil {
return nil, fmt.Errorf("%w: ElicitationHandler returned a nil result", jsonrpc2.ErrInternal)
}
// An accept without content answers nothing, so every field takes its
// default.
if res.Action == "accept" && schema != nil && res.Content == nil {
res.Content = map[string]any{}
}
// Validate elicitation result content against requested schema.
if res.Action == "accept" && schema != nil && res.Content != nil {
if res.Action == "accept" && schema != nil {
Comment on lines +778 to +784

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// An accept without content answers nothing, so every field takes its
// default.
if res.Action == "accept" && schema != nil && res.Content == nil {
res.Content = map[string]any{}
}
// Validate elicitation result content against requested schema.
if res.Action == "accept" && schema != nil && res.Content != nil {
if res.Action == "accept" && schema != nil {
// Validate elicitation result content against requested schema.
if res.Action == "accept" && schema != nil {
// An accept without content answers nothing, so every field takes its
// default.
if res.Content == nil {
res.Content = map[string]any{}
}

resolved, err := schema.Resolve(nil)
if err != nil {
return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to resolve requested schema: %v", err)}
Expand Down
73 changes: 73 additions & 0 deletions mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2006,6 +2006,79 @@ func TestElicitationDefaultValues(t *testing.T) {
}
}

// TestElicitationAcceptWithoutContent checks that an accept with nil content
// is treated as an empty answer: defaults are applied and the schema is still
// enforced.
func TestElicitationAcceptWithoutContent(t *testing.T) {
ctx := context.Background()
testcases := []struct {
name string
schema *jsonschema.Schema
expected map[string]any
wantErr bool
}{
{
name: "defaults are applied",
schema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"color": {Type: "string", Default: json.RawMessage(`"blue"`)},
"count": {Type: "integer", Default: json.RawMessage("3")},
},
},
expected: map[string]any{"color": "blue", "count": float64(3)},
},
{
name: "required property is enforced",
schema: &jsonschema.Schema{
Type: "object",
Required: []string{"name"},
Properties: map[string]*jsonschema.Schema{
"name": {Type: "string"},
},
},
wantErr: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ct, st := NewInMemoryTransports()
ss, err := NewServer(testImpl, nil).Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
c := NewClient(testImpl, &ClientOptions{
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept", Content: nil}, nil
},
})
cs, err := c.Connect(ctx, ct, &ClientSessionOptions{ProtocolVersion: protocolVersion20251125})
if err != nil {
t.Fatal(err)
}
defer cs.Close()

res, err := ss.Elicit(ctx, &ElicitParams{Message: tc.name, RequestedSchema: tc.schema})
if tc.wantErr {
if err == nil {
t.Fatalf("Elicit succeeded with content %v, want a validation error", res.Content)
}
if !strings.Contains(err.Error(), "does not match requested schema") {
t.Fatalf("Elicit error = %v, want a schema validation error", err)
}
return
}
if err != nil {
t.Fatalf("Elicit: %v", err)
}
if diff := cmp.Diff(tc.expected, res.Content); diff != "" {
t.Errorf("content mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestKeepAliveFailure(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx := context.Background()
Expand Down
Loading