Skip to content

Commit 36e3a67

Browse files
committed
mcp: remove tool genericity
DO NOT SUBMIT TESTS DO NOT PASS YET API changes to remove genericity from the tool call path. This makes it easier to write code that can deal with tools generally, like wrappers around a ToolHandler. Here is the go doc diff: --- /tmp/old.doc 2025-08-14 09:03:30.772292329 -0400 +++ /tmp/new.doc 2025-08-14 08:58:37.113063370 -0400 @@ -73,7 +73,7 @@ FUNCTIONS -func AddTool[In, Out any](s *Server, t *Tool, h ToolHandlerFor[In, Out]) +func AddTool[In, Out any](s *Server, t *Tool, h TypedToolHandler[In, Out]) AddTool adds a Tool to the server, or replaces one with the same name. If the tool's input schema is nil, it is set to the schema inferred from the In type parameter, using jsonschema.For. If the tool's output schema is @@ -81,6 +81,10 @@ schema is set to the schema inferred from Out. The Tool argument must not be modified after this call. + The handler should return the result as the second return value. The first + return value, a *CallToolResult, may be nil, or its fields other than + StructuredContent may be populated. + func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport) NewInMemoryTransports returns two [InMemoryTransports] that connect to each other. @@ -125,24 +129,28 @@ func (c AudioContent) MarshalJSON() ([]byte, error) -type CallToolParams = CallToolParamsFor[any] - -type CallToolParamsFor[In any] struct { +type CallToolParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` Name string `json:"name"` - Arguments In `json:"arguments,omitempty"` + Arguments any `json:"arguments,omitempty"` } -func (x *CallToolParamsFor[Out]) GetProgressToken() any +func (x *CallToolParams) GetProgressToken() any -func (x *CallToolParamsFor[Out]) SetProgressToken(t any) +func (x *CallToolParams) SetProgressToken(t any) -type CallToolResult = CallToolResultFor[any] - The server's response to a tool call. +func (c *CallToolParams) UnmarshalJSON(data []byte) error + When unmarshalling CallToolParams on the server side, we need to delay + unmarshaling of the arguments. -type CallToolResultFor[Out any] struct { +type CallToolRequest struct { + Session *ServerSession + Params *CallToolParams +} + +type CallToolResult struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. Meta `json:"_meta,omitempty"` @@ -151,7 +159,7 @@ Content []Content `json:"content"` // An optional JSON object that represents the structured result of the tool // call. - StructuredContent Out `json:"structuredContent,omitempty"` + StructuredContent any `json:"structuredContent,omitempty"` // Whether the tool call ended in an error. // // If not set, this is assumed to be false (the call was successful). @@ -166,8 +174,9 @@ // should be reported as an MCP error response. IsError bool `json:"isError,omitempty"` } + The server's response to a tool call. -func (x *CallToolResultFor[Out]) UnmarshalJSON(data []byte) error +func (x *CallToolResult) UnmarshalJSON(data []byte) error UnmarshalJSON handles the unmarshalling of content into the Content interface. @@ -283,7 +292,7 @@ Session *ClientSession Params P } - A ClientRequest is a request to a client. + A ClientRequest[P] is a request to a client. func (r *ClientRequest[P]) GetParams() Params @@ -1532,9 +1541,7 @@ type ServerSession struct { // Has unexported fields. } - A ServerSession is a logical connection from a single MCP client. - Its methods can be used to send requests or notifications to the client. - Create a session by calling Server.Connect. + a session by calling Server.Connect. Call ServerSession.Close to close the connection, or await client termination with ServerSession.Wait. @@ -1786,6 +1793,8 @@ // If not provided, Annotations.Title should be used for display if present, // otherwise Name. Title string `json:"title,omitempty"` + + // Has unexported fields. } Definition for a tool the client can call. @@ -1826,13 +1835,10 @@ Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers. -type ToolHandler = ToolHandlerFor[map[string]any, any] - A ToolHandler handles a call to tools/call. [CallToolParams.Arguments] will - contain a map[string]any that has been validated against the input schema. - -type ToolHandlerFor[In, Out any] func(context.Context, *ServerRequest[*CallToolParamsFor[In]]) (*CallToolResultFor[Out], error) - A ToolHandlerFor handles a call to tools/call with typed arguments and - results. +type ToolHandler func(ctx context.Context, req *ServerRequest[*CallToolParams], args any) (*CallToolResult, error) + A ToolHandler handles a call to tools/call. req.Params.Arguments will + contain a json.RawMessage containing the arguments. args will contain a + value that has been validated against the input schema. type ToolListChangedParams struct { // This property is reserved by the protocol to allow clients and servers to @@ -1856,6 +1862,10 @@ Transports should be used for at most one call to Server.Connect or Client.Connect. +type TypedToolHandler[In, Out any] func(context.Context, *ServerRequest[*CallToolParams], In) (*CallToolResult, Out, error) + A TypedToolHandler handles a call to tools/call with typed arguments and + results. + type UnsubscribeParams struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses.
1 parent a834f3c commit 36e3a67

File tree

14 files changed

+374
-368
lines changed

14 files changed

+374
-368
lines changed

examples/server/sse/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ type SayHiParams struct {
2424
Name string `json:"name"`
2525
}
2626

27-
func SayHi(ctx context.Context, req *mcp.ServerRequest[*mcp.CallToolParamsFor[SayHiParams]]) (*mcp.CallToolResultFor[any], error) {
28-
return &mcp.CallToolResultFor[any]{
27+
func SayHi(ctx context.Context, req *mcp.ServerRequest[*mcp.CallToolParams], args SayHiParams) (*mcp.CallToolResult, any, error) {
28+
return &mcp.CallToolResult{
2929
Content: []mcp.Content{
30-
&mcp.TextContent{Text: "Hi " + req.Params.Arguments.Name},
30+
&mcp.TextContent{Text: "Hi " + args.Name},
3131
},
32-
}, nil
32+
}, nil, nil
3333
}
3434

3535
func main() {

mcp/example_middleware_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,16 @@ func Example_loggingMiddleware() {
8989
},
9090
func(
9191
ctx context.Context,
92-
req *mcp.ServerRequest[*mcp.CallToolParamsFor[map[string]any]],
93-
) (*mcp.CallToolResultFor[any], error) {
94-
name, ok := req.Params.Arguments["name"].(string)
92+
req *mcp.ServerRequest[*mcp.CallToolParams],
93+
args any,
94+
) (*mcp.CallToolResult, error) {
95+
name, ok := args.(map[string]any)["name"].(string)
9596
if !ok {
9697
return nil, fmt.Errorf("name parameter is required and must be a string")
9798
}
9899

99100
message := fmt.Sprintf("Hello, %s!", name)
100-
return &mcp.CallToolResultFor[any]{
101+
return &mcp.CallToolResult{
101102
Content: []mcp.Content{
102103
&mcp.TextContent{Text: message},
103104
},

mcp/features_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ type SayHiParams struct {
1818
Name string `json:"name"`
1919
}
2020

21-
func SayHi(ctx context.Context, cc *ServerSession, params *CallToolParamsFor[SayHiParams]) (*CallToolResultFor[any], error) {
22-
return &CallToolResultFor[any]{
21+
func SayHi(ctx context.Context, req *ServerRequest[*CallToolParams], args SayHiParams) (*CallToolResult, any, error) {
22+
return &CallToolResult{
2323
Content: []Content{
24-
&TextContent{Text: "Hi " + params.Name},
24+
&TextContent{Text: "Hi " + args.Name},
2525
},
26-
}, nil
26+
}, nil, nil
2727
}
2828

2929
func TestFeatureSetOrder(t *testing.T) {
@@ -45,7 +45,7 @@ func TestFeatureSetOrder(t *testing.T) {
4545
fs := newFeatureSet(func(t *Tool) string { return t.Name })
4646
fs.add(tc.tools...)
4747
got := slices.Collect(fs.all())
48-
if diff := cmp.Diff(got, tc.want, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
48+
if diff := cmp.Diff(got, tc.want, cmpopts.IgnoreUnexported(jsonschema.Schema{}, Tool{})); diff != "" {
4949
t.Errorf("expected %v, got %v, (-want +got):\n%s", tc.want, got, diff)
5050
}
5151
}
@@ -69,7 +69,7 @@ func TestFeatureSetAbove(t *testing.T) {
6969
fs := newFeatureSet(func(t *Tool) string { return t.Name })
7070
fs.add(tc.tools...)
7171
got := slices.Collect(fs.above(tc.above))
72-
if diff := cmp.Diff(got, tc.want, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
72+
if diff := cmp.Diff(got, tc.want, cmpopts.IgnoreUnexported(jsonschema.Schema{}, Tool{})); diff != "" {
7373
t.Errorf("expected %v, got %v, (-want +got):\n%s", tc.want, got, diff)
7474
}
7575
}

mcp/mcp_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ type hiParams struct {
3232
// TODO(jba): after schemas are stateless (WIP), this can be a variable.
3333
func greetTool() *Tool { return &Tool{Name: "greet", Description: "say hi"} }
3434

35-
func sayHi(ctx context.Context, req *ServerRequest[*CallToolParamsFor[hiParams]]) (*CallToolResultFor[any], error) {
35+
func sayHi(ctx context.Context, req *ServerRequest[*CallToolParams], args hiParams) (*CallToolResult, any, error) {
3636
if err := req.Session.Ping(ctx, nil); err != nil {
37-
return nil, fmt.Errorf("ping failed: %v", err)
37+
return nil, nil, fmt.Errorf("ping failed: %v", err)
3838
}
39-
return &CallToolResultFor[any]{Content: []Content{&TextContent{Text: "hi " + req.Params.Arguments.Name}}}, nil
39+
return &CallToolResult{Content: []Content{&TextContent{Text: "hi " + args.Name}}}, nil, nil
4040
}
4141

4242
var codeReviewPrompt = &Prompt{
@@ -97,7 +97,7 @@ func TestEndToEnd(t *testing.T) {
9797
Description: "say hi",
9898
}, sayHi)
9999
s.AddTool(&Tool{Name: "fail", InputSchema: &jsonschema.Schema{}},
100-
func(context.Context, *ServerRequest[*CallToolParamsFor[map[string]any]]) (*CallToolResult, error) {
100+
func(context.Context, *ServerRequest[*CallToolParams], any) (*CallToolResult, error) {
101101
return nil, errTestFailure
102102
})
103103
s.AddPrompt(codeReviewPrompt, codReviewPromptHandler)
@@ -647,7 +647,7 @@ func TestCancellation(t *testing.T) {
647647
cancelled = make(chan struct{}, 1) // don't block the request
648648
)
649649

650-
slowRequest := func(ctx context.Context, req *ServerRequest[*CallToolParamsFor[map[string]any]]) (*CallToolResult, error) {
650+
slowRequest := func(ctx context.Context, _ *ServerRequest[*CallToolParams], _ any) (*CallToolResult, error) {
651651
start <- struct{}{}
652652
select {
653653
case <-ctx.Done():
@@ -836,7 +836,7 @@ func traceCalls[S Session](w io.Writer, prefix string) Middleware {
836836
}
837837
}
838838

839-
func nopHandler(context.Context, *ServerRequest[*CallToolParamsFor[map[string]any]]) (*CallToolResult, error) {
839+
func nopHandler(context.Context, *ServerRequest[*CallToolParams], any) (*CallToolResult, error) {
840840
return nil, nil
841841
}
842842

mcp/protocol.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,32 @@ type Annotations struct {
4040
Priority float64 `json:"priority,omitempty"`
4141
}
4242

43-
type CallToolParams = CallToolParamsFor[any]
44-
45-
type CallToolParamsFor[In any] struct {
43+
type CallToolParams struct {
4644
// This property is reserved by the protocol to allow clients and servers to
4745
// attach additional metadata to their responses.
4846
Meta `json:"_meta,omitempty"`
4947
Name string `json:"name"`
50-
Arguments In `json:"arguments,omitempty"`
48+
Arguments any `json:"arguments,omitempty"`
5149
}
5250

53-
// The server's response to a tool call.
54-
type CallToolResult = CallToolResultFor[any]
51+
// When unmarshalling CallToolParams on the server side, we need to delay unmarshaling of the arguments.
52+
func (c *CallToolParams) UnmarshalJSON(data []byte) error {
53+
var raw struct {
54+
Meta `json:"_meta,omitempty"`
55+
Name string `json:"name"`
56+
RawArguments json.RawMessage `json:"arguments,omitempty"`
57+
}
58+
if err := json.Unmarshal(data, &raw); err != nil {
59+
return err
60+
}
61+
c.Meta = raw.Meta
62+
c.Name = raw.Name
63+
c.Arguments = raw.RawArguments
64+
return nil
65+
}
5566

56-
type CallToolResultFor[Out any] struct {
67+
// The server's response to a tool call.
68+
type CallToolResult struct {
5769
// This property is reserved by the protocol to allow clients and servers to
5870
// attach additional metadata to their responses.
5971
Meta `json:"_meta,omitempty"`
@@ -62,7 +74,7 @@ type CallToolResultFor[Out any] struct {
6274
Content []Content `json:"content"`
6375
// An optional JSON object that represents the structured result of the tool
6476
// call.
65-
StructuredContent Out `json:"structuredContent,omitempty"`
77+
StructuredContent any `json:"structuredContent,omitempty"`
6678
// Whether the tool call ended in an error.
6779
//
6880
// If not set, this is assumed to be false (the call was successful).
@@ -78,12 +90,12 @@ type CallToolResultFor[Out any] struct {
7890
IsError bool `json:"isError,omitempty"`
7991
}
8092

81-
func (*CallToolResultFor[Out]) isResult() {}
93+
func (*CallToolResult) isResult() {}
8294

8395
// UnmarshalJSON handles the unmarshalling of content into the Content
8496
// interface.
85-
func (x *CallToolResultFor[Out]) UnmarshalJSON(data []byte) error {
86-
type res CallToolResultFor[Out] // avoid recursion
97+
func (x *CallToolResult) UnmarshalJSON(data []byte) error {
98+
type res CallToolResult // avoid recursion
8799
var wire struct {
88100
res
89101
Content []*wireContent `json:"content"`
@@ -95,13 +107,13 @@ func (x *CallToolResultFor[Out]) UnmarshalJSON(data []byte) error {
95107
if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil {
96108
return err
97109
}
98-
*x = CallToolResultFor[Out](wire.res)
110+
*x = CallToolResult(wire.res)
99111
return nil
100112
}
101113

102-
func (x *CallToolParamsFor[Out]) isParams() {}
103-
func (x *CallToolParamsFor[Out]) GetProgressToken() any { return getProgressToken(x) }
104-
func (x *CallToolParamsFor[Out]) SetProgressToken(t any) { setProgressToken(x, t) }
114+
func (x *CallToolParams) isParams() {}
115+
func (x *CallToolParams) GetProgressToken() any { return getProgressToken(x) }
116+
func (x *CallToolParams) SetProgressToken(t any) { setProgressToken(x, t) }
105117

106118
type CancelledParams struct {
107119
// This property is reserved by the protocol to allow clients and servers to
@@ -867,6 +879,8 @@ type Tool struct {
867879
// If not provided, Annotations.Title should be used for display if present,
868880
// otherwise Name.
869881
Title string `json:"title,omitempty"`
882+
883+
newArgs func() any
870884
}
871885

872886
// Additional properties describing a Tool to clients.

mcp/protocol_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ func TestCompleteReference(t *testing.T) {
208208
})
209209
}
210210
}
211+
211212
func TestCompleteParams(t *testing.T) {
212213
// Define test cases specifically for Marshalling
213214
marshalTests := []struct {
@@ -514,13 +515,15 @@ func TestContentUnmarshal(t *testing.T) {
514515
var got CallToolResult
515516
roundtrip(ctr, &got)
516517

517-
ctrf := &CallToolResultFor[int]{
518-
Meta: Meta{"m": true},
519-
Content: content,
520-
IsError: true,
521-
StructuredContent: 3,
518+
ctrf := &CallToolResult{
519+
Meta: Meta{"m": true},
520+
Content: content,
521+
IsError: true,
522+
// Ints become floats with zero fractional part when unmarshaled.
523+
// The jsoncschema package will validate these against a schema with type "integer".
524+
StructuredContent: float64(3),
522525
}
523-
var gotf CallToolResultFor[int]
526+
var gotf CallToolResult
524527
roundtrip(ctrf, &gotf)
525528

526529
pm := &PromptMessage{

mcp/server.go

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"context"
1010
"encoding/base64"
1111
"encoding/gob"
12-
"encoding/json"
1312
"fmt"
1413
"iter"
1514
"maps"
@@ -145,16 +144,7 @@ func (s *Server) RemovePrompts(names ...string) {
145144
// or one where any input is valid, set [Tool.InputSchema] to the empty schema,
146145
// &jsonschema.Schema{}.
147146
func (s *Server) AddTool(t *Tool, h ToolHandler) {
148-
if t.InputSchema == nil {
149-
// This prevents the tool author from forgetting to write a schema where
150-
// one should be provided. If we papered over this by supplying the empty
151-
// schema, then every input would be validated and the problem wouldn't be
152-
// discovered until runtime, when the LLM sent bad data.
153-
panic(fmt.Sprintf("adding tool %q: nil input schema", t.Name))
154-
}
155-
if err := addToolErr(s, t, h); err != nil {
156-
panic(err)
157-
}
147+
s.addServerTool(newServerTool(t, h))
158148
}
159149

160150
// AddTool adds a [Tool] to the server, or replaces one with the same name.
@@ -163,25 +153,24 @@ func (s *Server) AddTool(t *Tool, h ToolHandler) {
163153
// If the tool's output schema is nil and the Out type parameter is not the empty
164154
// interface, then the output schema is set to the schema inferred from Out.
165155
// The Tool argument must not be modified after this call.
166-
func AddTool[In, Out any](s *Server, t *Tool, h ToolHandlerFor[In, Out]) {
167-
if err := addToolErr(s, t, h); err != nil {
168-
panic(err)
169-
}
156+
//
157+
// The handler should return the result as the second return value. The first return value,
158+
// a *CallToolResult, may be nil, or its fields other than StructuredContent may be
159+
// populated.
160+
func AddTool[In, Out any](s *Server, t *Tool, h TypedToolHandler[In, Out]) {
161+
s.addServerTool(newTypedServerTool(t, h))
170162
}
171163

172-
func addToolErr[In, Out any](s *Server, t *Tool, h ToolHandlerFor[In, Out]) (err error) {
173-
defer util.Wrapf(&err, "adding tool %q", t.Name)
174-
st, err := newServerTool(t, h)
164+
func (s *Server) addServerTool(st *serverTool, err error) {
175165
if err != nil {
176-
return err
166+
panic(fmt.Sprintf("adding tool %q: %v", st.tool.Name, err))
177167
}
178168
// Assume there was a change, since add replaces existing tools.
179169
// (It's possible a tool was replaced with an identical one, but not worth checking.)
180170
// TODO: Batch these changes by size and time? The typescript SDK doesn't.
181171
// TODO: Surface notify error here? best not, in case we need to batch.
182172
s.changeAndNotify(notificationToolListChanged, &ToolListChangedParams{},
183173
func() bool { s.tools.add(st); return true })
184-
return nil
185174
}
186175

187176
// RemoveTools removes the tools with the given names.
@@ -326,7 +315,7 @@ func (s *Server) listTools(_ context.Context, req *ServerRequest[*ListToolsParam
326315
})
327316
}
328317

329-
func (s *Server) callTool(ctx context.Context, req *ServerRequest[*CallToolParamsFor[json.RawMessage]]) (*CallToolResult, error) {
318+
func (s *Server) callTool(ctx context.Context, req *ServerRequest[*CallToolParams]) (*CallToolResult, error) {
330319
s.mu.Lock()
331320
st, ok := s.tools.get(req.Params.Name)
332321
s.mu.Unlock()
@@ -612,7 +601,7 @@ func (ss *ServerSession) initialized(ctx context.Context, params *InitializedPar
612601
return nil, fmt.Errorf("duplicate %q received", notificationInitialized)
613602
}
614603
if h := ss.server.opts.InitializedHandler; h != nil {
615-
h(ctx, serverRequestFor(ss, params))
604+
h(ctx, newServerRequest(ss, params))
616605
}
617606
return nil, nil
618607
}
@@ -626,7 +615,7 @@ func (s *Server) callRootsListChangedHandler(ctx context.Context, req *ServerReq
626615

627616
func (ss *ServerSession) callProgressNotificationHandler(ctx context.Context, p *ProgressNotificationParams) (Result, error) {
628617
if h := ss.server.opts.ProgressNotificationHandler; h != nil {
629-
h(ctx, serverRequestFor(ss, p))
618+
h(ctx, newServerRequest(ss, p))
630619
}
631620
return nil, nil
632621
}

mcp/server_example_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ type SayHiParams struct {
1616
Name string `json:"name"`
1717
}
1818

19-
func SayHi(ctx context.Context, req *mcp.ServerRequest[*mcp.CallToolParamsFor[SayHiParams]]) (*mcp.CallToolResultFor[any], error) {
20-
return &mcp.CallToolResultFor[any]{
19+
func SayHi(ctx context.Context, req *mcp.ServerRequest[*mcp.CallToolParams], args SayHiParams) (*mcp.CallToolResult, any, error) {
20+
return &mcp.CallToolResult{
2121
Content: []mcp.Content{
22-
&mcp.TextContent{Text: "Hi " + req.Params.Arguments.Name},
22+
&mcp.TextContent{Text: "Hi " + args.Name},
2323
},
24-
}, nil
24+
}, nil, nil
2525
}
2626

2727
func ExampleServer() {

mcp/shared.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,10 +408,6 @@ func (r *ServerRequest[P]) GetSession() Session { return r.Session }
408408
func (r *ClientRequest[P]) GetParams() Params { return r.Params }
409409
func (r *ServerRequest[P]) GetParams() Params { return r.Params }
410410

411-
func serverRequestFor[P Params](s *ServerSession, p P) *ServerRequest[P] {
412-
return &ServerRequest[P]{Session: s, Params: p}
413-
}
414-
415411
func clientRequestFor[P Params](s *ClientSession, p P) *ClientRequest[P] {
416412
return &ClientRequest[P]{Session: s, Params: p}
417413
}

0 commit comments

Comments
 (0)