Skip to content

Commit 18c96d6

Browse files
authored
mcp: enforce input schema type "object" (#349)
The spec makes it clear that input schemas must have type "object". Enforce that. Allow "any" as an input argument type by special-casing it. Fixes #283.
1 parent 4587941 commit 18c96d6

File tree

4 files changed

+18
-9
lines changed

4 files changed

+18
-9
lines changed

mcp/mcp_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func TestEndToEnd(t *testing.T) {
9898
Name: "greet",
9999
Description: "say hi",
100100
}, sayHi)
101-
AddTool(s, &Tool{Name: "fail", InputSchema: &jsonschema.Schema{}},
101+
AddTool(s, &Tool{Name: "fail", InputSchema: &jsonschema.Schema{Type: "object"}},
102102
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
103103
return nil, nil, errTestFailure
104104
})
@@ -257,7 +257,7 @@ func TestEndToEnd(t *testing.T) {
257257
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
258258
}
259259

260-
s.AddTool(&Tool{Name: "T", InputSchema: &jsonschema.Schema{}}, nopHandler)
260+
s.AddTool(&Tool{Name: "T", InputSchema: &jsonschema.Schema{Type: "object"}}, nopHandler)
261261
waitForNotification(t, "tools")
262262
s.RemoveTools("T")
263263
waitForNotification(t, "tools")
@@ -697,7 +697,7 @@ func TestCancellation(t *testing.T) {
697697
return nil, nil, nil
698698
}
699699
cs, _ := basicConnection(t, func(s *Server) {
700-
AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{}}, slowRequest)
700+
AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{Type: "object"}}, slowRequest)
701701
})
702702
defer cs.Close()
703703

@@ -1496,8 +1496,8 @@ func TestAddTool_DuplicateNoPanicAndNoDuplicate(t *testing.T) {
14961496
// Use two distinct Tool instances with the same name but different
14971497
// descriptions to ensure the second replaces the first
14981498
// This case was written specifically to reproduce a bug where duplicate tools where causing jsonschema errors
1499-
t1 := &Tool{Name: "dup", Description: "first", InputSchema: &jsonschema.Schema{}}
1500-
t2 := &Tool{Name: "dup", Description: "second", InputSchema: &jsonschema.Schema{}}
1499+
t1 := &Tool{Name: "dup", Description: "first", InputSchema: &jsonschema.Schema{Type: "object"}}
1500+
t2 := &Tool{Name: "dup", Description: "second", InputSchema: &jsonschema.Schema{Type: "object"}}
15011501
s.AddTool(t1, nopHandler)
15021502
s.AddTool(t2, nopHandler)
15031503
})

mcp/server.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
162162
// discovered until runtime, when the LLM sent bad data.
163163
panic(fmt.Errorf("AddTool %q: missing input schema", t.Name))
164164
}
165+
if t.InputSchema.Type != "object" {
166+
panic(fmt.Errorf(`AddTool %q: input schema must have type "object"`, t.Name))
167+
}
165168
st := &serverTool{tool: t, handler: h}
166169
// Assume there was a change, since add replaces existing tools.
167170
// (It's possible a tool was replaced with an identical one, but not worth checking.)
@@ -190,6 +193,12 @@ func ToolFor[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandle
190193
// TODO(v0.3.0): test
191194
func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler, error) {
192195
tt := *t
196+
197+
// Special handling for an "any" input: treat as an empty object.
198+
if reflect.TypeFor[In]() == reflect.TypeFor[any]() && t.InputSchema == nil {
199+
tt.InputSchema = &jsonschema.Schema{Type: "object"}
200+
}
201+
193202
var inputResolved *jsonschema.Resolved
194203
if _, err := setSchema[In](&tt.InputSchema, &inputResolved); err != nil {
195204
return nil, nil, fmt.Errorf("input schema: %w", err)

mcp/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ func TestServerPaginateVariousPageSizes(t *testing.T) {
232232
}
233233

234234
func TestServerCapabilities(t *testing.T) {
235-
tool := &Tool{Name: "t", InputSchema: &jsonschema.Schema{}}
235+
tool := &Tool{Name: "t", InputSchema: &jsonschema.Schema{Type: "object"}}
236236
testCases := []struct {
237237
name string
238238
configureServer func(s *Server)

mcp/streamable_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ func testClientReplay(t *testing.T, test clientReplayTest) {
219219
// proxy-killing action.
220220
serverReadyToKillProxy := make(chan struct{})
221221
serverClosed := make(chan struct{})
222-
AddTool(server, &Tool{Name: "multiMessageTool", InputSchema: &jsonschema.Schema{}},
222+
AddTool(server, &Tool{Name: "multiMessageTool", InputSchema: &jsonschema.Schema{Type: "object"}},
223223
func(ctx context.Context, req *CallToolRequest, args map[string]any) (*CallToolResult, any, error) {
224224
// Send one message to the request context, and another to a background
225225
// context (which will end up on the hanging GET).
@@ -353,7 +353,7 @@ func TestServerInitiatedSSE(t *testing.T) {
353353
t.Fatalf("client.Connect() failed: %v", err)
354354
}
355355
defer clientSession.Close()
356-
AddTool(server, &Tool{Name: "testTool", InputSchema: &jsonschema.Schema{}},
356+
AddTool(server, &Tool{Name: "testTool", InputSchema: &jsonschema.Schema{Type: "object"}},
357357
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
358358
return &CallToolResult{}, nil, nil
359359
})
@@ -658,7 +658,7 @@ func TestStreamableServerTransport(t *testing.T) {
658658
// behavior, if any.
659659
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
660660
server.AddTool(
661-
&Tool{Name: "tool", InputSchema: &jsonschema.Schema{}},
661+
&Tool{Name: "tool", InputSchema: &jsonschema.Schema{Type: "object"}},
662662
func(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) {
663663
if test.tool != nil {
664664
test.tool(t, ctx, req.Session)

0 commit comments

Comments
 (0)