internal/mcp: change CallTool signature

CallTool takes a tool name, arguments, and an options struct.

Also, pass the params to ToolHandler.

Update the design doc accordingly.

Change-Id: I21b7995b607bea976ad708ebb6ac0716018f39cb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/672596
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Jonathan Amsterdam 2025-05-14 07:01:59 -04:00
commit 6394485a3d
9 changed files with 37 additions and 30 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}