Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,38 +60,37 @@ That example uses a `bytes.Buffer`, but you can also log to a file, or to

## Inspecting HTTP traffic

There are a couple different ways to investigate traffic to an HTTP transport
([streamable](protocol.md#streamable-transport) or legacy SSE).

The first is to use an HTTP middleware:
For the [streamable HTTP transport](protocol.md#streamable-transport), use
`StreamableHTTPOptions.OnRequestSummary` to inspect decoded message metadata
without recording request parameters:

```go
func ExampleStreamableHTTPHandler_middleware() {
func ExampleStreamableHTTPHandler_requestSummary() {
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{Stateless: true})
loggingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Example debugging; you could also capture the response.
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatal(err)
}
req.Body.Close() // ignore error
req.Body = io.NopCloser(bytes.NewBuffer(body))
fmt.Println(req.Method, string(body))
handler.ServeHTTP(w, req)
}, &mcp.StreamableHTTPOptions{
Stateless: true,
OnRequestSummary: func(_ context.Context, summary mcp.StreamableHTTPRequestSummary) {
fmt.Printf("method=%q notification=%t response=%t\n",
summary.Method, summary.IsNotification, summary.IsResponse)
},
})
httpServer := httptest.NewServer(loggingHandler)
httpServer := httptest.NewServer(handler)
defer httpServer.Close()

// The SDK is currently permissive of some missing keys in "params".
mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL)
// Output:
// POST {"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}
// method="initialize" notification=false response=false
}
```

The second is to use a general purpose tool to inspect http traffic, such as
The callback runs before validation and dispatch. It does not run for batches
or requests rejected before message decoding. To observe those failures, wrap
the handler in HTTP middleware that records properties such as the HTTP method
and status code without reading or logging the request body.

For full HTTP traffic, including legacy SSE, use a general-purpose tool such as
[wireshark](https://www.wireshark.org/) or
[tcpdump](https://linux.die.net/man/8/tcpdump).
14 changes: 9 additions & 5 deletions internal/docs/troubleshooting.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ That example uses a `bytes.Buffer`, but you can also log to a file, or to

## Inspecting HTTP traffic

There are a couple different ways to investigate traffic to an HTTP transport
([streamable](protocol.md#streamable-transport) or legacy SSE).
For the [streamable HTTP transport](protocol.md#streamable-transport), use
`StreamableHTTPOptions.OnRequestSummary` to inspect decoded message metadata
without recording request parameters:

The first is to use an HTTP middleware:
%include ../../mcp/streamable_example_test.go requestsummary -

%include ../../mcp/streamable_example_test.go httpmiddleware -
The callback runs before validation and dispatch. It does not run for batches
or requests rejected before message decoding. To observe those failures, wrap

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe every pre-callback rejection gap

For a stateless or ephemeral request whose EventStore.Open fails, ephemeralConnectOpts has already decoded the valid single-message body before connectStreamable returns without calling StreamableServerTransport.ServeHTTP, so OnRequestSummary is skipped even though this is neither a batch nor a pre-decoding rejection. The stated boundary therefore gives operators the wrong expectation for connection failures; describe the gap as any HTTP, authorization, session-routing, or connection rejection before the callback, then regenerate the published page.

AGENTS.md reference: AGENTS.md:L55-L58

Useful? React with 👍 / 👎.

the handler in HTTP middleware that records properties such as the HTTP method
and status code without reading or logging the request body.

The second is to use a general purpose tool to inspect http traffic, such as
For full HTTP traffic, including legacy SSE, use a general-purpose tool such as
[wireshark](https://www.wireshark.org/) or
[tcpdump](https://linux.die.net/man/8/tcpdump).
29 changes: 12 additions & 17 deletions mcp/streamable_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package mcp_test

import (
"bytes"
"context"
"fmt"
"io"
"log"
Expand Down Expand Up @@ -40,34 +40,29 @@ func ExampleStreamableHTTPHandler() {

// !-streamablehandler

// !+httpmiddleware
// !+requestsummary

func ExampleStreamableHTTPHandler_middleware() {
func ExampleStreamableHTTPHandler_requestSummary() {
server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.1.0"}, nil)
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{Stateless: true})
loggingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Example debugging; you could also capture the response.
body, err := io.ReadAll(req.Body)
if err != nil {
log.Fatal(err)
}
req.Body.Close() // ignore error
req.Body = io.NopCloser(bytes.NewBuffer(body))
fmt.Println(req.Method, string(body))
handler.ServeHTTP(w, req)
}, &mcp.StreamableHTTPOptions{
Stateless: true,
OnRequestSummary: func(_ context.Context, summary mcp.StreamableHTTPRequestSummary) {
fmt.Printf("method=%q notification=%t response=%t\n",
summary.Method, summary.IsNotification, summary.IsResponse)
},
})
httpServer := httptest.NewServer(loggingHandler)
httpServer := httptest.NewServer(handler)
defer httpServer.Close()

// The SDK is currently permissive of some missing keys in "params".
mustPostMessage(`{"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}`, httpServer.URL)
// Output:
// POST {"jsonrpc": "2.0", "id": 1, "method":"initialize", "params": {}}
// method="initialize" notification=false response=false
}

// !-httpmiddleware
// !-requestsummary

func mustPostMessage(msg, url string) string {
req := orFatal(http.NewRequest("POST", url, strings.NewReader(msg)))
Expand Down