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:
parent
4721f501df
commit
6394485a3d
9 changed files with 37 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue