Skip to content

Commit 4f7cf70

Browse files
committed
mcp: allow any value for schemas
Experiment WIP DO NOT REVIEW DO NOT SUBMIT
1 parent 33ff851 commit 4f7cf70

File tree

6 files changed

+77
-27
lines changed

6 files changed

+77
-27
lines changed

mcp/client.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult,
295295
}
296296

297297
// Validate that the requested schema only contains top-level properties without nesting
298-
if err := validateElicitSchema(req.Params.RequestedSchema); err != nil {
298+
schema, err := validateElicitSchema(req.Params.RequestedSchema)
299+
if err != nil {
299300
return nil, jsonrpc2.NewError(CodeInvalidParams, err.Error())
300301
}
301302

@@ -305,11 +306,11 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult,
305306
}
306307

307308
// Validate elicitation result content against requested schema
308-
if req.Params.RequestedSchema != nil && res.Content != nil {
309+
if schema != nil && res.Content != nil {
309310
// TODO: is this the correct behavior if validation fails?
310311
// It isn't the *server's* params that are invalid, so why would we return
311312
// this code to the server?
312-
resolved, err := req.Params.RequestedSchema.Resolve(nil)
313+
resolved, err := schema.Resolve(nil)
313314
if err != nil {
314315
return nil, jsonrpc2.NewError(CodeInvalidParams, fmt.Sprintf("failed to resolve requested schema: %v", err))
315316
}
@@ -324,14 +325,23 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult,
324325

325326
// validateElicitSchema validates that the schema conforms to MCP elicitation schema requirements.
326327
// Per the MCP specification, elicitation schemas are limited to flat objects with primitive properties only.
327-
func validateElicitSchema(schema *jsonschema.Schema) error {
328-
if schema == nil {
329-
return nil // nil schema is allowed
328+
func validateElicitSchema(wireSchema any) (*jsonschema.Schema, error) {
329+
if wireSchema == nil {
330+
return nil, nil // nil schema is allowed
331+
}
332+
333+
data, err := json.Marshal(wireSchema)
334+
if err != nil {
335+
return nil, err
336+
}
337+
var schema *jsonschema.Schema
338+
if err := json.Unmarshal(data, &schema); err != nil {
339+
return nil, err
330340
}
331341

332342
// The root schema must be of type "object" if specified
333343
if schema.Type != "" && schema.Type != "object" {
334-
return fmt.Errorf("elicit schema must be of type 'object', got %q", schema.Type)
344+
return nil, fmt.Errorf("elicit schema must be of type 'object', got %q", schema.Type)
335345
}
336346

337347
// Check if the schema has properties
@@ -342,12 +352,12 @@ func validateElicitSchema(schema *jsonschema.Schema) error {
342352
}
343353

344354
if err := validateElicitProperty(propName, propSchema); err != nil {
345-
return err
355+
return nil, err
346356
}
347357
}
348358
}
349359

350-
return nil
360+
return schema, nil
351361
}
352362

353363
// validateElicitProperty validates a single property in an elicitation schema.

mcp/client_list_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package mcp_test
66

77
import (
88
"context"
9+
"encoding/json"
910
"iter"
1011
"log"
1112
"testing"
@@ -41,7 +42,13 @@ func TestList(t *testing.T) {
4142
if err != nil {
4243
t.Fatal(err)
4344
}
44-
tt.InputSchema = is
45+
data, err := json.Marshal(is)
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
if err := json.Unmarshal(data, &tt.InputSchema); err != nil {
50+
t.Fatal(err)
51+
}
4552
wantTools = append(wantTools, tt)
4653
}
4754
t.Run("list", func(t *testing.T) {

mcp/protocol.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ package mcp
1313
import (
1414
"encoding/json"
1515
"fmt"
16-
17-
"github.com/google/jsonschema-go/jsonschema"
1816
)
1917

2018
// Optional annotations for the client. The client can use annotations to inform
@@ -914,13 +912,23 @@ type Tool struct {
914912
// tools. It can be thought of like a "hint" to the model.
915913
Description string `json:"description,omitempty"`
916914
// A JSON Schema object defining the expected parameters for the tool.
917-
InputSchema *jsonschema.Schema `json:"inputSchema"`
915+
//
916+
// From the server, this field may be set to any value that can marshal to
917+
// JSON.
918+
// From the client, this field will use the default JSON marshaling (a
919+
// map[string]any).
920+
InputSchema any `json:"inputSchema"`
918921
// Intended for programmatic or logical use, but used as a display name in past
919922
// specs or fallback (if title isn't present).
920923
Name string `json:"name"`
921924
// An optional JSON Schema object defining the structure of the tool's output
922925
// returned in the structuredContent field of a CallToolResult.
923-
OutputSchema *jsonschema.Schema `json:"outputSchema,omitempty"`
926+
//
927+
// From the server, this field may be set to any value that can marshal to
928+
// JSON.
929+
// From the client, this field will use the default JSON marshaling (a
930+
// map[string]any).
931+
OutputSchema any `json:"outputSchema,omitempty"`
924932
// Intended for UI and end-user contexts — optimized to be human-readable and
925933
// easily understood, even by those unfamiliar with domain-specific terminology.
926934
// If not provided, Annotations.Title should be used for display if present,
@@ -1022,9 +1030,15 @@ type ElicitParams struct {
10221030
Meta `json:"_meta,omitempty"`
10231031
// The message to present to the user.
10241032
Message string `json:"message"`
1025-
// A restricted subset of JSON Schema.
1033+
// A JSON schema object defining the requested elicitation schema.
1034+
//
1035+
// From the server, this field may be set to any value that can marshal to
1036+
// JSON.
1037+
// From the client, this field will use the default JSON marshaling (a
1038+
// map[string]any).
1039+
//
10261040
// Only top-level properties are allowed, without nesting.
1027-
RequestedSchema *jsonschema.Schema `json:"requestedSchema"`
1041+
RequestedSchema any `json:"requestedSchema"`
10281042
}
10291043

10301044
func (x *ElicitParams) isParams() {}

mcp/server.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,10 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
189189
// discovered until runtime, when the LLM sent bad data.
190190
panic(fmt.Errorf("AddTool %q: missing input schema", t.Name))
191191
}
192-
if t.InputSchema.Type != "object" {
192+
if s, ok := t.InputSchema.(*jsonschema.Schema); ok && s.Type != "object" {
193193
panic(fmt.Errorf(`AddTool %q: input schema must have type "object"`, t.Name))
194194
}
195-
if t.OutputSchema != nil && t.OutputSchema.Type != "object" {
195+
if s, ok := t.OutputSchema.(*jsonschema.Schema); ok && s.Type != "object" {
196196
panic(fmt.Errorf(`AddTool %q: output schema must have type "object"`, t.Name))
197197
}
198198
st := &serverTool{tool: t, handler: h}
@@ -331,20 +331,32 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
331331
//
332332
// TODO(rfindley): we really shouldn't ever return 'null' results. Maybe we
333333
// should have a jsonschema.Zero(schema) helper?
334-
func setSchema[T any](sfield **jsonschema.Schema, rfield **jsonschema.Resolved) (zero any, err error) {
334+
func setSchema[T any](sfield *any, rfield **jsonschema.Resolved) (zero any, err error) {
335+
var internalSchema *jsonschema.Schema
335336
if *sfield == nil {
336337
rt := reflect.TypeFor[T]()
337338
if rt.Kind() == reflect.Pointer {
338339
rt = rt.Elem()
339340
zero = reflect.Zero(rt).Interface()
340341
}
341342
// TODO: we should be able to pass nil opts here.
342-
*sfield, err = jsonschema.ForType(rt, &jsonschema.ForOptions{})
343+
internalSchema, err = jsonschema.ForType(rt, &jsonschema.ForOptions{})
344+
if err == nil {
345+
*sfield = internalSchema
346+
}
347+
} else {
348+
data, err := json.Marshal(*sfield)
349+
if err != nil {
350+
return zero, err
351+
}
352+
if err := json.Unmarshal(data, &internalSchema); err != nil {
353+
return zero, err
354+
}
343355
}
344356
if err != nil {
345357
return zero, err
346358
}
347-
*rfield, err = (*sfield).Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
359+
*rfield, err = internalSchema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true})
348360
return zero, err
349361
}
350362

mcp/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ func TestAddTool(t *testing.T) {
492492

493493
type schema = jsonschema.Schema
494494

495-
func testToolForSchema[In, Out any](t *testing.T, tool *Tool, in string, out Out, wantIn, wantOut *schema, wantErrContaining string) {
495+
func testToolForSchema[In, Out any](t *testing.T, tool *Tool, in string, out Out, wantIn, wantOut any, wantErrContaining string) {
496496
t.Helper()
497497
th := func(context.Context, *CallToolRequest, In) (*CallToolResult, Out, error) {
498498
return nil, out, nil

mcp/tool_example_test.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func ExampleAddTool_customMarshalling() {
6868
}
6969
// Output:
7070
// my_tool {
71-
// "type": "object",
71+
// "additionalProperties": false,
7272
// "properties": {
7373
// "end": {
7474
// "type": "string"
@@ -80,7 +80,7 @@ func ExampleAddTool_customMarshalling() {
8080
// "type": "string"
8181
// }
8282
// },
83-
// "additionalProperties": false
83+
// "type": "object"
8484
// }
8585
}
8686

@@ -200,9 +200,9 @@ func ExampleAddTool_complexSchema() {
200200
}
201201
// Formatting the entire schemas would be too much output.
202202
// Just check that our customizations were effective.
203-
fmt.Println("max days:", *t.InputSchema.Properties["days"].Maximum)
204-
fmt.Println("max confidence:", *t.OutputSchema.Properties["confidence"].Maximum)
205-
fmt.Println("weather types:", t.OutputSchema.Properties["dailyForecast"].Items.Properties["type"].Enum)
203+
fmt.Println("max days:", jsonPath(t.InputSchema, "properties", "days", "maximum"))
204+
fmt.Println("max confidence:", jsonPath(t.OutputSchema, "properties", "confidence", "maximum"))
205+
fmt.Println("weather types:", jsonPath(t.OutputSchema, "properties", "dailyForecast", "items", "properties", "type", "enum"))
206206
}
207207
// Output:
208208
// max days: 10
@@ -218,3 +218,10 @@ func connect(ctx context.Context, server *mcp.Server) (*mcp.ClientSession, error
218218
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil)
219219
return client.Connect(ctx, t2, nil)
220220
}
221+
222+
func jsonPath(s any, path ...string) any {
223+
if len(path) == 0 {
224+
return s
225+
}
226+
return jsonPath(s.(map[string]any)[path[0]], path[1:]...)
227+
}

0 commit comments

Comments
 (0)