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",
|
||||
Description: "say hi",
|
||||
}, 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) {
|
||||
return nil, nil, errTestFailure
|
||||
})
|
||||
|
|
@ -257,7 +257,7 @@ func TestEndToEnd(t *testing.T) {
|
|||
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")
|
||||
s.RemoveTools("T")
|
||||
waitForNotification(t, "tools")
|
||||
|
|
@ -697,7 +697,7 @@ func TestCancellation(t *testing.T) {
|
|||
return nil, nil, nil
|
||||
}
|
||||
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()
|
||||
|
||||
|
|
@ -1496,8 +1496,8 @@ func TestAddTool_DuplicateNoPanicAndNoDuplicate(t *testing.T) {
|
|||
// Use two distinct Tool instances with the same name but different
|
||||
// descriptions to ensure the second replaces the first
|
||||
// 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{}}
|
||||
t2 := &Tool{Name: "dup", Description: "second", InputSchema: &jsonschema.Schema{}}
|
||||
t1 := &Tool{Name: "dup", Description: "first", InputSchema: &jsonschema.Schema{Type: "object"}}
|
||||
t2 := &Tool{Name: "dup", Description: "second", InputSchema: &jsonschema.Schema{Type: "object"}}
|
||||
s.AddTool(t1, 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.
|
||||
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}
|
||||
// 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.)
|
||||
|
|
@ -190,6 +193,12 @@ func ToolFor[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandle
|
|||
// TODO(v0.3.0): test
|
||||
func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHandler, error) {
|
||||
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
|
||||
if _, err := setSchema[In](&tt.InputSchema, &inputResolved); err != nil {
|
||||
return nil, nil, fmt.Errorf("input schema: %w", err)
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ func TestServerPaginateVariousPageSizes(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 {
|
||||
name string
|
||||
configureServer func(s *Server)
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ func testClientReplay(t *testing.T, test clientReplayTest) {
|
|||
// proxy-killing action.
|
||||
serverReadyToKillProxy := 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) {
|
||||
// Send one message to the request context, and another to a background
|
||||
// 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)
|
||||
}
|
||||
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) {
|
||||
return &CallToolResult{}, nil, nil
|
||||
})
|
||||
|
|
@ -658,7 +658,7 @@ func TestStreamableServerTransport(t *testing.T) {
|
|||
// behavior, if any.
|
||||
server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
|
||||
server.AddTool(
|
||||
&Tool{Name: "tool", InputSchema: &jsonschema.Schema{}},
|
||||
&Tool{Name: "tool", InputSchema: &jsonschema.Schema{Type: "object"}},
|
||||
func(ctx context.Context, req *CallToolRequest) (*CallToolResult, error) {
|
||||
if test.tool != nil {
|
||||
test.tool(t, ctx, req.Session)
|
||||
|
|
|
|||
Loading…
Reference in a new issue