diff --git a/design/design.md b/design/design.md index 3791f0f..083fb49 100644 --- a/design/design.md +++ b/design/design.md @@ -329,9 +329,8 @@ corresponding to the union of all properties for union elements. For brevity, only a few examples are shown here: ```go -type CallToolParams struct { - Arguments map[string]json.RawMessage `json:"arguments,omitempty"` - Name string `json:"name"` +type ReadResourceParams struct { + URI string `json:"uri"` } type CallToolResult struct { @@ -428,10 +427,7 @@ transport := mcp.NewCommandTransport(exec.Command("myserver")) session, err := client.Connect(ctx, transport) if err != nil { ... } // Call a tool on the server. -content, err := session.CallTool(ctx, &CallToolParams{ - Name: "greet", - Arguments: map[string]any{"name": "you"} , -}) +content, err := session.CallTool(ctx, "greet", map[string]any{"name": "you"}, nil}) ... return session.Close() ``` @@ -479,17 +475,20 @@ documentation. ### Spec Methods -As we saw above, the `ClientSession` method for the specification's -`CallTool` RPC takes a context and a params pointer as arguments, and returns a -result pointer and error: +In our SDK, RPC methods that are defined in the specification take a context and +a params pointer as arguments, and return a result pointer and error: ```go -func (*ClientSession) CallTool(context.Context, *CallToolParams) (*CallToolResult, error) +func (*ClientSession) ListTools(context.Context, *ListToolsParams) (*ListToolsResult, error) ``` -Our SDK has a method for every RPC in the spec, and their signatures all share -this form. To avoid boilerplate, we don't repeat this signature for RPCs -defined in the spec; readers may assume it when we mention a "spec method." +Our SDK has a method for every RPC in the spec, and except for `CallTool`, +their signatures all share this form. To avoid boilerplate, we don't repeat this +signature for RPCs defined in the spec; readers may assume it when we mention a +"spec method." + +`CallTool` is the only exception: for convenience, it takes the tool name and +arguments, with an options truct for additional request fields. Why do we use params instead of the full request? JSON-RPC requests consist of a method name and a set of parameters, and the method is already encoded in the Go method name. @@ -564,7 +563,7 @@ can cancel an operation by cancelling the associated context: ```go ctx, cancel := context.WithCancel(ctx) -go session.CallTool(ctx, "slow", map[string]any{}) +go session.CallTool(ctx, "slow", map[string]any{}, nil) cancel() ``` @@ -687,7 +686,7 @@ type Tool struct { Name string `json:"name"` } -type ToolHandler func(context.Context, *ServerSession, map[string]json.RawMessage) (*CallToolResult, error) +type ToolHandler func(context.Context, *ServerSession, *CallToolParams) (*CallToolResult, error) type ServerTool struct { Tool Tool diff --git a/mcp/client.go b/mcp/client.go index d4dfba6..0a24930 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -174,7 +174,8 @@ func (c *Client) ListTools(ctx context.Context, params *ListToolsParams) (*ListT } // CallTool calls the tool with the given name and arguments. -func (c *Client) CallTool(ctx context.Context, name string, args map[string]any) (_ *CallToolResult, err error) { +// Pass a [CallToolOptions] to provide additional request fields. +func (c *Client) CallTool(ctx context.Context, name string, args map[string]any, opts *CallToolOptions) (_ *CallToolResult, err error) { defer func() { if err != nil { err = fmt.Errorf("calling tool %q: %w", name, err) @@ -196,6 +197,13 @@ func (c *Client) CallTool(ctx context.Context, name string, args map[string]any) return standardCall[CallToolResult](ctx, c.conn, "tools/call", params) } +// NOTE: the following struct should consist of all fields of callToolParams except name and arguments. + +// CallToolOptions contains options to [Client.CallTools]. +type CallToolOptions struct { + ProgressToken any // string or int +} + // ListResources lists the resources that are currently available on the server. func (c *Client) ListResources(ctx context.Context, params *ListResourcesParams) (*ListResourcesResult, error) { return standardCall[ListResourcesResult](ctx, c.conn, "resources/list", params) diff --git a/mcp/cmd_test.go b/mcp/cmd_test.go index addc59e..fd048f4 100644 --- a/mcp/cmd_test.go +++ b/mcp/cmd_test.go @@ -52,7 +52,7 @@ func TestCmdTransport(t *testing.T) { if err := client.Start(ctx); err != nil { log.Fatal(err) } - got, err := client.CallTool(ctx, "greet", map[string]any{"name": "user"}) + got, err := client.CallTool(ctx, "greet", map[string]any{"name": "user"}, nil) if err != nil { log.Fatal(err) } diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go index 2d494b3..b487483 100644 --- a/mcp/mcp_test.go +++ b/mcp/mcp_test.go @@ -160,7 +160,7 @@ func TestEndToEnd(t *testing.T) { t.Fatalf("tools/list mismatch (-want +got):\n%s", diff) } - gotHi, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}) + gotHi, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}, nil) if err != nil { t.Fatal(err) } @@ -171,7 +171,7 @@ func TestEndToEnd(t *testing.T) { t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff) } - gotFail, err := c.CallTool(ctx, "fail", map[string]any{}) + gotFail, err := c.CallTool(ctx, "fail", map[string]any{}, nil) // Counter-intuitively, when a tool fails, we don't expect an RPC error for // call tool: instead, the failure is embedded in the result. if err != nil { @@ -317,12 +317,12 @@ func TestServerClosing(t *testing.T) { } wg.Done() }() - if _, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}); err != nil { + if _, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}, nil); err != nil { t.Fatalf("after connecting: %v", err) } cc.Close() wg.Wait() - if _, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}); !errors.Is(err, ErrConnectionClosed) { + if _, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}, nil); !errors.Is(err, ErrConnectionClosed) { t.Errorf("after disconnection, got error %v, want EOF", err) } } @@ -384,7 +384,7 @@ func TestCancellation(t *testing.T) { defer sc.Close() ctx, cancel := context.WithCancel(context.Background()) - go sc.CallTool(ctx, "slow", map[string]any{}) + go sc.CallTool(ctx, "slow", map[string]any{}, nil) <-start cancel() select { diff --git a/mcp/server.go b/mcp/server.go index ce6ef81..18d4d09 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -204,7 +204,7 @@ func (s *Server) callTool(ctx context.Context, cc *ServerConnection, params *Cal if !ok { return nil, fmt.Errorf("%s: unknown tool %q", jsonrpc2.ErrInvalidParams, params.Name) } - return tool.Handler(ctx, cc, params.Arguments) + return tool.Handler(ctx, cc, params) } func (s *Server) listResources(_ context.Context, _ *ServerConnection, params *ListResourcesParams) (*ListResourcesResult, error) { diff --git a/mcp/server_example_test.go b/mcp/server_example_test.go index 04042c1..b7ec788 100644 --- a/mcp/server_example_test.go +++ b/mcp/server_example_test.go @@ -39,7 +39,7 @@ func ExampleServer() { log.Fatal(err) } - res, err := client.CallTool(ctx, "greet", map[string]any{"name": "user"}) + res, err := client.CallTool(ctx, "greet", map[string]any{"name": "user"}, nil) if err != nil { log.Fatal(err) } diff --git a/mcp/sse_example_test.go b/mcp/sse_example_test.go index eee822f..60bf3f4 100644 --- a/mcp/sse_example_test.go +++ b/mcp/sse_example_test.go @@ -40,7 +40,7 @@ func ExampleSSEHandler() { } defer client.Close() - res, err := client.CallTool(ctx, "add", map[string]any{"x": 1, "y": 2}) + res, err := client.CallTool(ctx, "add", map[string]any{"x": 1, "y": 2}, nil) if err != nil { log.Fatal(err) } diff --git a/mcp/sse_test.go b/mcp/sse_test.go index 5cbd2cc..fc844a4 100644 --- a/mcp/sse_test.go +++ b/mcp/sse_test.go @@ -43,7 +43,7 @@ func TestSSEServer(t *testing.T) { t.Fatal(err) } cc := <-conns - gotHi, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}) + gotHi, err := c.CallTool(ctx, "greet", map[string]any{"name": "user"}, nil) if err != nil { t.Fatal(err) } diff --git a/mcp/tool.go b/mcp/tool.go index 7a93c6c..2cc9a50 100644 --- a/mcp/tool.go +++ b/mcp/tool.go @@ -13,7 +13,7 @@ import ( ) // A ToolHandler handles a call to tools/call. -type ToolHandler func(context.Context, *ServerConnection, map[string]json.RawMessage) (*CallToolResult, error) +type ToolHandler func(context.Context, *ServerConnection, *CallToolParams) (*CallToolResult, error) // A Tool is a tool definition that is bound to a tool handler. type ServerTool struct { @@ -39,10 +39,10 @@ func NewTool[TReq any](name, description string, handler func(context.Context, * if err != nil { panic(err) } - wrapped := func(ctx context.Context, cc *ServerConnection, args map[string]json.RawMessage) (*CallToolResult, error) { + wrapped := func(ctx context.Context, cc *ServerConnection, params *CallToolParams) (*CallToolResult, error) { // For simplicity, just marshal and unmarshal the arguments. // This could be avoided in the future. - rawArgs, err := json.Marshal(args) + rawArgs, err := json.Marshal(params.Arguments) if err != nil { return nil, err }