Streamable HTTP: a tool result that fails to marshal leaves the POST hanging (no JSON-RPC error)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 45/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- go
- Ambito
- api, networking
Direzione di ricerca
Start in mcp/streamable.go at streamableServerConn.Write (around line 1824), where EncodeMessage fails before the response reaches the pending HTTP request. Then check how jsonrpc2 stores that error as writeErr. Run the reproduction with Stateless: true and confirm the third POST for id 2 hangs. Done means that POST returns a JSON-RPC error with code -32603 instead of timing out, and the encoding error is logged.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
If a tool handler returns a CallToolResult that json.Marshal cannot encode, the Streamable HTTP handler never answers the request. The client waits until its own timeout. No JSON-RPC error is sent, and the server keeps no trace the client can see.
This happens with Stateless: true and with Stateless: false. A normal tool on the same server answers right away. One example of an unencodable value is a float64 that is +Inf or NaN in StructuredContent. A cyclic value has the same effect.
To Reproduce
go-sdk v1.8.0:
package main
import (
"bytes"
"context"
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
server := mcp.NewServer(&mcp.Implementation{Name: "repro", Version: "0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "ok", Description: "returns a normal result"},
func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "fine"}}}, nil, nil
})
mcp.AddTool(server, &mcp.Tool{Name: "bad", Description: "returns a result json.Marshal rejects"},
func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "has +Inf"}},
StructuredContent: map[string]any{"value": math.Inf(1)}, // json: unsupported value: +Inf
}, nil, nil
})
ts := httptest.NewServer(mcp.NewStreamableHTTPHandler(
func(*http.Request) *mcp.Server { return server }, &mcp.StreamableHTTPOptions{Stateless: true}))
defer ts.Close()
client := &http.Client{Timeout: 5 * time.Second}
post := func(body string) {
req, _ := http.NewRequest(http.MethodPost, ts.URL, bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Mcp-Protocol-Version", "2025-11-25")
start := time.Now()
resp, err := client.Do(req)
if err != nil {
fmt.Printf("no response after %s: %v\n", time.Since(start).Round(time.Millisecond), err)
return
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Printf("HTTP %d in %s: %.120s\n", resp.StatusCode, time.Since(start).Round(time.Millisecond), bytes.TrimSpace(out))
}
post(`{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"repro","version":"0"}}}`)
post(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ok","arguments":{}}}`)
post(`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"bad","arguments":{}}}`)
}
Output:
HTTP 200 in 3ms: event: message data: {"jsonrpc":"2.0","id":0,"result":{...}}
HTTP 200 in 1ms: event: message data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"fine"}]}}
no response after 5.004s: Post "http://127.0.0.1:41245": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
With Stateless: false (initialize, then notifications/initialized with the session ID), the result is the same.
Expected behavior
The request completes with a JSON-RPC error for id 2, for example -32603 "internal error: result could not be encoded". It must not hang. Ideally the server also logs the encoding error.
Where it seems to happen
streamableServerConn.Write encodes the message first (mcp/streamable.go, around line 1824 in v1.8.0) and returns the EncodeMessage error. At that point the response has not yet been routed to the pending HTTP request. On the jsonrpc2 side, the error is only kept as the connection's writeErr. The handler that waits for the response to this request ID is never released.
A possible fix: when a *jsonrpc.Response fails to encode, replace it with an error response for the same ID and write that instead. The other option is to encode the result in the request path before handing it to the writer, so the error can become a JSON-RPC error.
Workaround
On the server, marshal the StructuredContent (or the whole result) before returning it from the handler, and return a tool error when that fails.
Environment
- go-sdk v1.8.0 (latest at the time of writing)
- Go 1.27.1
- Linux
- Lingua principale
- Go
- Stelle
- 5.2k
- Fork
- 568
- Merge medio
- 1g 15h
- PR unite (30g)
- 39
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di modelcontextprotocol/go-sdk
-
Session-lifecycle bookkeeping logs at info, spamming multiple log lines per request on stateless streamable HTTPForse già presa @anneheartrecord l’ha presa 42 giorni fa. ApertaP3
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
modelcontextprotocol/go-sdk#1204 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensionsForse già presa @ajuijas l’ha presa 208 giorni fa. Apertaneeds investigation
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
modelcontextprotocol/go-sdk#745 · 13 commenti · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
-
mcp: client returns an input_required result that has only requestState instead of retryingForse già presa @SergeevDmitry l’ha presa oggi. Aperta
Difficoltà 3/5 Mezza giornata Idoneità per principianti 62/100
modelcontextprotocol/go-sdk#1364 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 18/100
modelcontextprotocol/go-sdk#1359 ·
I maintainer di solito rispondono entro 1 giorno
-
auth: Authorize fails against a 2025-03-26 server whose URL has a query stringForse già presa @akshita317 l’ha presa 3 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 40/100
modelcontextprotocol/go-sdk#1347 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di modelcontextprotocol/go-sdk
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
mvanhorn/cli-printing-press#4980 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
priority: p3 type: feat
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
googleapis/librarian#7775 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
OpenTollGate/tollgate-module-basic-go#833 ·
I maintainer di solito rispondono entro 1 giorno
-
documentation
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno