Skip to content

Commit a69f3c0

Browse files
committed
Merge branch 'main' into fgrosse/479-connection-closing-callback
2 parents eab40f7 + b636b16 commit a69f3c0

17 files changed

+346
-24
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ software development kit (SDK) for the Model Context Protocol (MCP).
2121
2222
## Package documentation
2323

24-
The SDK consists of three importable packages:
24+
The SDK consists of several importable packages:
2525

2626
- The
2727
[`github.com/modelcontextprotocol/go-sdk/mcp`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp)
@@ -34,6 +34,10 @@ The SDK consists of three importable packages:
3434
[`github.com/modelcontextprotocol/go-sdk/auth`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth)
3535
package provides some primitives for supporting oauth.
3636

37+
- The
38+
[`github.com/modelcontextprotocol/go-sdk/oauthex`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/oauthex)
39+
package provides extensions to the OAuth protocol, such as ProtectedResourceMetadata.
40+
3741
## Getting started
3842

3943
To get started creating an MCP server, create an `mcp.Server` instance, add

docs/troubleshooting.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ func ExampleLoggingTransport() {
3939
if _, err := client.Connect(ctx, logTransport, nil); err != nil {
4040
log.Fatal(err)
4141
}
42-
fmt.Println(b.String())
42+
// Sort for stability: reads are concurrent to writes.
43+
for _, line := range slices.Sorted(strings.SplitSeq(b.String(), "\n")) {
44+
fmt.Println(line)
45+
}
46+
4347
// Output:
44-
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18"}}
4548
// read: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"logging":{}},"protocolVersion":"2025-06-18","serverInfo":{"name":"server","version":"v0.0.1"}}}
49+
// write: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"roots":{"listChanged":true}},"clientInfo":{"name":"client","version":"v0.0.1"},"protocolVersion":"2025-06-18"}}
4650
// write: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
47-
4851
}
4952
```
5053

internal/readme/README.src.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ software development kit (SDK) for the Model Context Protocol (MCP).
2020
2121
## Package documentation
2222

23-
The SDK consists of three importable packages:
23+
The SDK consists of several importable packages:
2424

2525
- The
2626
[`github.com/modelcontextprotocol/go-sdk/mcp`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp)
@@ -33,6 +33,10 @@ The SDK consists of three importable packages:
3333
[`github.com/modelcontextprotocol/go-sdk/auth`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth)
3434
package provides some primitives for supporting oauth.
3535

36+
- The
37+
[`github.com/modelcontextprotocol/go-sdk/oauthex`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/oauthex)
38+
package provides extensions to the OAuth protocol, such as ProtectedResourceMetadata.
39+
3640
## Getting started
3741

3842
To get started creating an MCP server, create an `mcp.Server` instance, add

mcp/cmd_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func TestCmdTransport(t *testing.T) {
226226
&mcp.TextContent{Text: "Hi user"},
227227
},
228228
}
229-
if diff := cmp.Diff(want, got); diff != "" {
229+
if diff := cmp.Diff(want, got, ctrCmpOpts...); diff != "" {
230230
t.Errorf("greet returned unexpected content (-want +got):\n%s", diff)
231231
}
232232
if err := session.Close(); err != nil {

mcp/content_nil_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestContentUnmarshalNil(t *testing.T) {
7272
}
7373

7474
// Verify that the Content field was properly populated
75-
if cmp.Diff(tt.want, tt.content) != "" {
75+
if cmp.Diff(tt.want, tt.content, ctrCmpOpts...) != "" {
7676
t.Errorf("Content is not equal: %v", cmp.Diff(tt.content, tt.content))
7777
}
7878
})
@@ -222,3 +222,5 @@ func TestContentUnmarshalNilWithInvalidContent(t *testing.T) {
222222
})
223223
}
224224
}
225+
226+
var ctrCmpOpts = []cmp.Option{cmp.AllowUnexported(mcp.CallToolResult{})}

mcp/event_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ func BenchmarkMemoryEventStore(b *testing.B) {
287287
payload := make([]byte, test.datasize)
288288
start := time.Now()
289289
b.ResetTimer()
290-
for i := 0; i < b.N; i++ {
290+
for i := range b.N {
291291
sessionID := sessionIDs[i%len(sessionIDs)]
292292
streamID := streamIDs[i%len(sessionIDs)][i%3]
293293
store.Append(ctx, sessionID, streamID, payload)

mcp/mcp_test.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ func TestEndToEnd(t *testing.T) {
234234
&TextContent{Text: "hi user"},
235235
},
236236
}
237-
if diff := cmp.Diff(wantHi, gotHi); diff != "" {
237+
if diff := cmp.Diff(wantHi, gotHi, ctrCmpOpts...); diff != "" {
238238
t.Errorf("tools/call 'greet' mismatch (-want +got):\n%s", diff)
239239
}
240240

@@ -253,7 +253,7 @@ func TestEndToEnd(t *testing.T) {
253253
&TextContent{Text: errTestFailure.Error()},
254254
},
255255
}
256-
if diff := cmp.Diff(wantFail, gotFail); diff != "" {
256+
if diff := cmp.Diff(wantFail, gotFail, ctrCmpOpts...); diff != "" {
257257
t.Errorf("tools/call 'fail' mismatch (-want +got):\n%s", diff)
258258
}
259259

@@ -1717,7 +1717,7 @@ func TestPointerArgEquivalence(t *testing.T) {
17171717
if err != nil {
17181718
t.Fatal(err)
17191719
}
1720-
if diff := cmp.Diff(r0, r1); diff != "" {
1720+
if diff := cmp.Diff(r0, r1, ctrCmpOpts...); diff != "" {
17211721
t.Errorf("CallTool(%v) with no arguments mismatch (-%s +%s):\n%s", args, t0.Name, t1.Name, diff)
17221722
}
17231723
}
@@ -1733,7 +1733,7 @@ func TestPointerArgEquivalence(t *testing.T) {
17331733
if err != nil {
17341734
t.Fatal(err)
17351735
}
1736-
if diff := cmp.Diff(r0, r1); diff != "" {
1736+
if diff := cmp.Diff(r0, r1, ctrCmpOpts...); diff != "" {
17371737
t.Errorf("CallTool({\"In\": %q}) mismatch (-%s +%s):\n%s", in, t0.Name, t1.Name, diff)
17381738
}
17391739
})
@@ -1837,3 +1837,70 @@ func TestEmbeddedStructResponse(t *testing.T) {
18371837
t.Errorf("CallTool() failed: %v", err)
18381838
}
18391839
}
1840+
1841+
func TestToolErrorMiddleware(t *testing.T) {
1842+
ctx := context.Background()
1843+
ct, st := NewInMemoryTransports()
1844+
1845+
s := NewServer(testImpl, nil)
1846+
AddTool(s, &Tool{
1847+
Name: "greet",
1848+
Description: "say hi",
1849+
}, sayHi)
1850+
AddTool(s, &Tool{Name: "fail", InputSchema: &jsonschema.Schema{Type: "object"}},
1851+
func(context.Context, *CallToolRequest, map[string]any) (*CallToolResult, any, error) {
1852+
return nil, nil, errTestFailure
1853+
})
1854+
1855+
var middleErr error
1856+
s.AddReceivingMiddleware(func(h MethodHandler) MethodHandler {
1857+
return func(ctx context.Context, method string, req Request) (Result, error) {
1858+
res, err := h(ctx, method, req)
1859+
if err == nil {
1860+
if ctr, ok := res.(*CallToolResult); ok {
1861+
middleErr = ctr.getError()
1862+
}
1863+
}
1864+
return res, err
1865+
}
1866+
})
1867+
_, err := s.Connect(ctx, st, nil)
1868+
if err != nil {
1869+
t.Fatal(err)
1870+
}
1871+
client := NewClient(&Implementation{Name: "test-client"}, nil)
1872+
clientSession, err := client.Connect(ctx, ct, nil)
1873+
if err != nil {
1874+
t.Fatal(err)
1875+
}
1876+
defer clientSession.Close()
1877+
1878+
_, err = clientSession.CallTool(ctx, &CallToolParams{
1879+
Name: "greet",
1880+
Arguments: map[string]any{"Name": "al"},
1881+
})
1882+
if err != nil {
1883+
t.Errorf("CallTool() failed: %v", err)
1884+
}
1885+
if middleErr != nil {
1886+
t.Errorf("middleware got error %v, want nil", middleErr)
1887+
}
1888+
res, err := clientSession.CallTool(ctx, &CallToolParams{
1889+
Name: "fail",
1890+
})
1891+
if err != nil {
1892+
t.Errorf("CallTool() failed: %v", err)
1893+
}
1894+
if !res.IsError {
1895+
t.Fatal("want error, got none")
1896+
}
1897+
// Clients can't see the error, because it isn't marshaled.
1898+
if err := res.getError(); err != nil {
1899+
t.Fatalf("got %v, want nil", err)
1900+
}
1901+
if middleErr != errTestFailure {
1902+
t.Errorf("middleware got err %v, want errTestFailure", middleErr)
1903+
}
1904+
}
1905+
1906+
var ctrCmpOpts = []cmp.Option{cmp.AllowUnexported(CallToolResult{})}

mcp/protocol.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,26 @@ type CallToolResult struct {
103103
// tool handler returns an error, and the error string is included as text in
104104
// the Content field.
105105
IsError bool `json:"isError,omitempty"`
106+
107+
// The error passed to setError, if any.
108+
// It is not marshaled, and therefore it is only visible on the server.
109+
// Its only use is in server sending middleware, where it can be accessed
110+
// with getError.
111+
err error
106112
}
107113

108114
// TODO(#64): consider exposing setError (and getError), by adding an error
109115
// field on CallToolResult.
110116
func (r *CallToolResult) setError(err error) {
111117
r.Content = []Content{&TextContent{Text: err.Error()}}
112118
r.IsError = true
119+
r.err = err
120+
}
121+
122+
// getError returns the error set with setError, or nil if none.
123+
// This function always returns nil on clients.
124+
func (r *CallToolResult) getError() error {
125+
return r.err
113126
}
114127

115128
func (*CallToolResult) isResult() {}

mcp/protocol_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ func TestContentUnmarshal(t *testing.T) {
499499
if err := json.Unmarshal(data, out); err != nil {
500500
t.Fatal(err)
501501
}
502-
if diff := cmp.Diff(in, out); diff != "" {
502+
if diff := cmp.Diff(in, out, ctrCmpOpts...); diff != "" {
503503
t.Errorf("mismatch (-want, +got):\n%s", diff)
504504
}
505505
}

mcp/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
292292
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content.
293293
if res.Content == nil {
294294
res.Content = []Content{&TextContent{
295-
Text: string(outbytes),
295+
Text: string(outJSON),
296296
}}
297297
}
298298
}

0 commit comments

Comments
 (0)