MCP SSE /message can return 202 even when the response is later dropped by a full SSE event queue
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 58/100
Línea de trabajo
Comienza por processMCPSSEMpscMessages en core/controller/mcp/mcp.go y compara su manejo de errores con la ruta embebida en core/controller/mcp/embedmcp.go. Lee SSEServer.HandleMessage en core/mcpproxy/sse.go y el flujo de /message en mcp.go, y luego reproduce el desbordamiento con un lector SSE bloqueado. Se considera terminado cuando los fallos de la cola de respuestas sean visibles o se gestionen según un comportamiento acordado, con cobertura del caso de desbordamiento.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
In the MCP SSE flow, POST /message?sessionId=... can return 202 Accepted after the request is placed into the per-session MPSC queue. The actual MCP response is produced later in a background goroutine and then enqueued into SSEServer.eventQueue.
If the SSE event queue is full, SSEServer.HandleMessage returns event queue is full, but processMCPSSEMpscMessages currently ignores that error and continues. From the MCP client's perspective, the POST request was accepted, but the corresponding JSON-RPC response may never arrive on the SSE connection.
Code path
Current main at 4963684929bfbb9071aea4400fd75593336bf7a7:
More detailed trace:
-
The routes are split between the SSE connection and the message endpoint.
22 router.GET("/sse", middleware.MCPAuth, mcp.HostMCPSSEServer) 23 router.POST("/message", mcp.MCPMessage)GET /sseopens the SSE channel, whilePOST /messagesends later JSON-RPC messages for that session. -
When the SSE connection is opened, a session and an
SSEServerare created:core/controller/mcp/mcp.go#L30-L52:30 // Store the session 31 store := getStore() 32 newSession := store.New() 33 34 newEndpoint := endpoint.NewEndpoint(newSession) 35 server := mcpproxy.NewSSEServer( 36 s, 37 mcpproxy.WithMessageEndpoint(newEndpoint), 38 ) 39 40 store.Set(newSession, mcpType) ... 48 // Start message processing goroutine 49 go processMCPSSEMpscMessages(ctx, newSession, server) 50 51 // Handle SSE connection 52 server.ServeHTTP(c.Writer, c.Request)This means the POST handler and the SSE writer are connected asynchronously through the session.
-
The SSE server has a bounded event queue:
49 // NewSSEServer creates a new SSE server instance with the given MCP server and options. 50 func NewSSEServer(server mcpservers.Server, opts ...SSEOption) *SSEServer { 51 s := &SSEServer{ 52 server: server, 53 messageEndpoint: "/message", 54 keepAlive: false, 55 keepAliveInterval: 30 * time.Second, 56 eventQueue: make(chan string, 100), 57 }core/mcpproxy/sse.go#L123-L132:123 // Main event loop - this runs in the HTTP handler goroutine 124 for { 125 select { 126 case event := <-s.eventQueue: 127 // Write the event to the response 128 fmt.Fprint(w, event) 129 flusher.Flush() 130 case <-r.Context().Done(): 131 return 132 }ServeHTTPdrainseventQueueand writes events to the client. If the SSE client is slow or blocked, this queue can fill. -
POST /messageonly confirms that the request was put into the request-side MPSC queue:core/controller/mcp/mcp.go#L115-L136:115 func sendMCPSSEMessage(c *gin.Context, sessionID string) { 116 _, ok := getStore().Get(sessionID) 117 if !ok { 118 http.Error(c.Writer, "invalid session", http.StatusBadRequest) 119 return 120 } ... 130 err = mpscInstance.send(c.Request.Context(), sessionID, body) 131 if err != nil { 132 http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 133 return 134 } 135 136 c.Writer.WriteHeader(http.StatusAccepted)core/controller/mcp/mcp-mpsc.go#L126-L136:126 func (c *channelMCPMpsc) send(ctx context.Context, id string, data []byte) error { 127 ch := c.getOrCreateChannel(id) 128 129 select { 130 case ch <- data: 131 return nil 132 case <-ctx.Done(): 133 return ctx.Err() 134 default: 135 return fmt.Errorf("channel buffer full for session %s", id) 136 }The request-side queue can report backpressure to
/message, but this does not cover the later response enqueue into SSE. -
The background goroutine then processes the request and tries to enqueue the response:
core/controller/mcp/mcp.go#L61-L75:61 mpscInstance := getMCPMpsc() 62 for { 63 select { 64 case <-ctx.Done(): 65 return 66 default: 67 data, err := mpscInstance.recv(ctx, sessionID) 68 if err != nil { 69 return 70 } 71 72 if err := server.HandleMessage(ctx, data); err != nil { 73 continue 74 } 75 }core/mcpproxy/sse.go#L138-L160:138 func (s *SSEServer) HandleMessage(ctx context.Context, req []byte) error { 139 // Process message through MCPServer 140 response := s.server.HandleMessage(ctx, req) ... 153 // Queue the event for sending via SSE 154 select { 155 case s.eventQueue <- message: 156 // Event queued successfully 157 default: 158 // Queue is full 159 return errors.New("event queue is full") 160 }The response-side enqueue can fail when
eventQueueis full. -
That response-side enqueue error is currently swallowed:
core/controller/mcp/mcp.go#L72-L74:72 if err := server.HandleMessage(ctx, data); err != nil { 73 continue 74 }Therefore
event queue is fullis not surfaced to the client and is not logged here.
So the failure can happen after /message has already accepted the request. In other words:
- request enqueue succeeds;
/messagereturns202 Accepted;- the MCP server processes the request;
- response enqueue into the SSE event queue fails;
- the error is ignored by the background loop;
- the client may never receive the response.
The same SSE processing pattern also appears in the embedded MCP SSE path:
Why this matters
MCP clients and agent frameworks rely on request/response pairing for tool calls. If a response is silently dropped:
- the client may wait until timeout even though the server processed the request;
- an agent may treat a tool call as failed or stuck;
- repeated requests can create duplicated side effects if the client retries;
- operators have no visible signal that the SSE session lost responses.
This is especially likely to matter with slow SSE clients, transient network backpressure, or bursty MCP tool calls.
For a relay/proxy, this kind of failure is hard to diagnose because the upstream MCP server may have processed the message correctly, while the downstream client only sees a missing SSE response.
Suggested fixes
Some possible directions:
- Do not silently ignore
server.HandleMessageerrors inprocessMCPSSEMpscMessages; log them with session context at minimum. - Consider closing/marking the SSE session unhealthy when
eventQueueoverflows, so clients reconnect instead of waiting for a missing response. - If possible, apply backpressure before returning
202 Accepted, or otherwise make response delivery failure visible to the client. - Track a dropped/overflow counter so operators can detect this condition.
Reproduction idea
One possible test:
- Open an MCP SSE session.
- Make the SSE reader slow or blocked so
eventQueueis not drained quickly. - POST more than 100 JSON-RPC requests to
/message?sessionId=.... - Observe that some POSTs may return
202 Accepted, while their corresponding JSON-RPC responses never appear on the SSE stream.
I may be missing intended behavior here, but it looks like an accepted MCP request can currently lose its response without any visible error.
- Lenguaje dominante
- Go
- Estrellas
- 565
- Forks
- 118
- Merge medio
- 5 min
- PR fusionados (30 d)
- 3
Preparar el entorno
- Incluye un Dockerfile o un archivo de Docker Compose
- Sin plantilla de pull request
- Sin guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de labring/aiproxy
-
Dificultad 4/5 3-5 días Aptitud para principiantes 22/100
-
功能请求:LOG_SQL_DSN 支持 ClickhouseAbierto
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 58/100
-
功能请求:增加渠道请求的并发限制Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 52/100
Todos los issues de labring/aiproxy
Issues similares
-
automation doc-healer documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 90/100
tsukumogami/tsuku#2804 ·
Los mantenedores suelen responder en 1 día
-
Anthropic /v1/messages: only the last thinking block of an assistant message is keptPosiblemente ocupada @Shayan12456 la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
ollama/ollama#19415 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
raised-by:verifier
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
medici-finance/assay#5039 ·
Los mantenedores suelen responder en 1 día
-
metax CheckHealth reads Status.Capacity, so unhealthy Metax-GPU nodes are never cleaned upPosiblemente ocupada @barbierajput378-pixel la tomó hoy. Abiertokind/bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Project-HAMi/HAMi#3343 · 1 comentario ·
Los mantenedores suelen responder en 1 día