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.
This commit is contained in:
parent
4587941994
commit
18c96d6912
4 changed files with 18 additions and 9 deletions
|
|
@ -98,7 +98,7 @@ func TestEndToEnd(t *testing.T) {
|
||||||
Name: "greet",
|
Name: "greet",
|
||||||
Description: "say hi",
|
Description: "say hi",
|
||||||
}, sayHi)
|
}, sayHi)
|
||||||
AddTool(s, &Tool{Name: "fail", InputSchema: &jsonschema.Schema{}},
|
AddTool(s, &Tool{Name: "fail", InputSchema: &jsonschema.Schema{Type: "object"}},
|
||||||
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
|
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
|
||||||
return nil, nil, errTestFailure
|
return nil, nil, errTestFailure
|
||||||
})
|
})
|
||||||
|
|
@ -257,7 +257,7 @@ func TestEndToEnd(t *testing.T) {
|
||||||
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
|
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AddTool(&Tool{Name: "T", InputSchema: &jsonschema.Schema{}}, nopHandler)
|
s.AddTool(&Tool{Name: "T", InputSchema: &jsonschema.Schema{Type: "object"}}, nopHandler)
|
||||||
waitForNotification(t, "tools")
|
waitForNotification(t, "tools")
|
||||||
s.RemoveTools("T")
|
s.RemoveTools("T")
|
||||||
waitForNotification(t, "tools")
|
waitForNotification(t, "tools")
|
||||||
|
|
@ -697,7 +697,7 @@ func TestCancellation(t *testing.T) {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
cs, _ := basicConnection(t, func(s *Server) {
|
cs, _ := basicConnection(t, func(s *Server) {
|
||||||
AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{}}, slowRequest)
|
AddTool(s, &Tool{Name: "slow", InputSchema: &jsonschema.Schema{Type: "object"}}, slowRequest)
|
||||||
})
|
})
|
||||||
defer cs.Close()
|
defer cs.Close()
|
||||||
|
|
||||||
|
|
@ -1496,8 +1496,8 @@ func TestAddTool_DuplicateNoPanicAndNoDuplicate(t *testing.T) {
|
||||||
// Use two distinct Tool instances with the same name but different
|
// Use two distinct Tool instances with the same name but different
|
||||||
// descriptions to ensure the second replaces the first
|
// descriptions to ensure the second replaces the first
|
||||||
// This case was written specifically to reproduce a bug where duplicate tools where causing jsonschema errors
|
// This case was written specifically to reproduce a bug where duplicate tools where causing jsonschema errors
|
||||||
t1 := &Tool{Name: "dup", Description: "first", InputSchema: &jsonschema.Schema{}}
|
t1 := &Tool{Name: "dup", Description: "first", InputSchema: &jsonschema.Schema{Type: "object"}}
|
||||||
t2 := &Tool{Name: "dup", Description: "second", InputSchema: &jsonschema.Schema{}}
|
t2 := &Tool{Name: "dup", Description: "second", InputSchema: &jsonschema.Schema{Type: "object"}}
|
||||||
s.AddTool(t1, nopHandler)
|
s.AddTool(t1, nopHandler)
|
||||||
s.AddTool(t2, nopHandler)
|
s.AddTool(t2, nopHandler)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,9 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
|
||||||
// discovered until runtime, when the LLM sent bad data.
|
// discovered until runtime, when the LLM sent bad data.
|
||||||
panic(fmt.Errorf("AddTool %q: missing input schema", t.Name))
|
panic(fmt.Errorf("AddTool %q: missing input schema", t.Name))
|
||||||
}
|
}
|
||||||
|
if t.InputSchema.Type != "object" {
|
||||||
|
panic(fmt.Errorf(`AddTool %q: input schema must have type "object"`, t.Name))
|
||||||
|
}
|
||||||
st := &serverTool{tool: t, handler: h}
|
st := &serverTool{tool: t, handler: h}
|
||||||
// Assume there was a change, since add replaces existing tools.
|
// Assume there was a change, since add replaces existing tools.
|
||||||
// (It's possible a tool was replaced with an identical one, but not worth checking.)
|
// (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
|
||||||
// TODO(v0.3.0): test
|
// TODO(v0.3.0): test
|
||||||
func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler, error) {
|
func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler, error) {
|
||||||
tt := *t
|
tt := *t
|
||||||
|
|
||||||
|
// Special handling for an "any" input: treat as an empty object.
|
||||||
|
if reflect.TypeFor[In]() == reflect.TypeFor[any]() && t.InputSchema == nil {
|
||||||
|
tt.InputSchema = &jsonschema.Schema{Type: "object"}
|
||||||
|
}
|
||||||
|
|
||||||
var inputResolved *jsonschema.Resolved
|
var inputResolved *jsonschema.Resolved
|
||||||
if _, err := setSchema[In](&tt.InputSchema, &inputResolved); err != nil {
|
if _, err := setSchema[In](&tt.InputSchema, &inputResolved); err != nil {
|
||||||
return nil, nil, fmt.Errorf("input schema: %w", err)
|
return nil, nil, fmt.Errorf("input schema: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ func TestServerPaginateVariousPageSizes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerCapabilities(t *testing.T) {
|
func TestServerCapabilities(t *testing.T) {
|
||||||
tool := &Tool{Name: "t", InputSchema: &jsonschema.Schema{}}
|
tool := &Tool{Name: "t", InputSchema: &jsonschema.Schema{Type: "object"}}
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
configureServer func(s *Server)
|
configureServer func(s *Server)
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ func testClientReplay(t *testing.T, test clientReplayTest) {
|
||||||
// proxy-killing action.
|
// proxy-killing action.
|
||||||
serverReadyToKillProxy := make(chan struct{})
|
serverReadyToKillProxy := make(chan struct{})
|
||||||
serverClosed := make(chan struct{})
|
serverClosed := make(chan struct{})
|
||||||
AddTool(server, &Tool{Name: "multiMessageTool", InputSchema: &jsonschema.Schema{}},
|
AddTool(server, &Tool{Name: "multiMessageTool", InputSchema: &jsonschema.Schema{Type: "object"}},
|
||||||
func(ctx context.Context, req *CallToolRequest, args map[string]any) (*CallToolResult, any, error) {
|
func(ctx context.Context, req *CallToolRequest, args map[string]any) (*CallToolResult, any, error) {
|
||||||
// Send one message to the request context, and another to a background
|
// Send one message to the request context, and another to a background
|
||||||
// context (which will end up on the hanging GET).
|
// context (which will end up on the hanging GET).
|
||||||
|
|
@ -353,7 +353,7 @@ func TestServerInitiatedSSE(t *testing.T) {
|
||||||
t.Fatalf("client.Connect() failed: %v", err)
|
t.Fatalf("client.Connect() failed: %v", err)
|
||||||
}
|
}
|
||||||
defer clientSession.Close()
|
defer clientSession.Close()
|
||||||
AddTool(server, &Tool{Name: "testTool", InputSchema: &jsonschema.Schema{}},
|
AddTool(server, &Tool{Name: "testTool", InputSchema: &jsonschema.Schema{Type: "object"}},
|
||||||
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
|
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
|
||||||
return &CallToolResult{}, nil, nil
|
return &CallToolResult{}, nil, nil
|
||||||
})
|
})
|
||||||
|
|
@ -658,7 +658,7 @@ func TestStreamableServerTransport(t *testing.T) {
|
||||||
// behavior, if any.
|
// behavior, if any.
|
||||||
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
|
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
|
||||||
server.AddTool(
|
server.AddTool(
|
||||||
&Tool{Name: "tool", InputSchema: &jsonschema.Schema{}},
|
&Tool{Name: "tool", InputSchema: &jsonschema.Schema{Type: "object"}},
|
||||||
func(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) {
|
func(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) {
|
||||||
if test.tool != nil {
|
if test.tool != nil {
|
||||||
test.tool(t, ctx, req.Session)
|
test.tool(t, ctx, req.Session)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue