Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Streamable HTTP: a tool result that fails to marshal leaves the POST hanging (no JSON-RPC error)

Aperta
#1,358 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

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

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di modelcontextprotocol/go-sdk

Tutte le issue di modelcontextprotocol/go-sdk

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.