SSEClientTransport drops path prefix when endpoint URL is not on the HTTP root (similar to python-sdk#563)
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
- #741 de @majiayu000 — cerrado sin fusionar
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 50/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Estancado
- Stack tecnológico
- go
- Área
- api, networking
Línea de trabajo
Comienza en SSEClientTransport.Connect, donde el primer evento de Endpoint se resuelve a partir del Endpoint configurado, y compara las URLs de reproducción documentadas con la URL del mensaje resultante. Añade cobertura para un prefijo de ruta que no sea raíz y para un evento de Endpoint que comience con /; se considera terminado cuando los POSTs posteriores conservan el prefijo de enrutamiento sin romper la resolución normal del Endpoint.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Describe the bug
SSEClientTransport drops the path prefix of the configured Endpoint when resolving the message endpoint from the first endpoint SSE event.
In Connect, the message endpoint is derived like this:
// Connect connects through the client endpoint.
func (c *SSEClientTransport) Connect(ctx context.Context) (Connection, error) {
parsedURL, err := url.Parse(c.Endpoint)
if err != nil {
return nil, fmt.Errorf("invalid endpoint: %v", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", c.Endpoint, nil)
if err != nil {
return nil, err
}
httpClient := c.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
req.Header.Set("Accept", "text/event-stream")
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
msgEndpoint, err := func() (*url.URL, error) {
var evt Event
for evt, err = range scanEvents(resp.Body) {
break
}
if err != nil {
return nil, err
}
if evt.Name != "endpoint" {
return nil, fmt.Errorf("first event is %q, want %q", evt.Name, "endpoint")
}
raw := string(evt.Data)
return parsedURL.Parse(raw)
}()
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("missing endpoint: %v", err)
}
// From here on, the stream takes ownership of resp.Body.
s := &sseClientConn{
client: httpClient,
msgEndpoint: msgEndpoint,
incoming: make(chan []byte, 100),
body: resp.Body,
done: make(chan struct{}),
}
go func() {
defer s.Close() // close the transport when the GET exits
for evt, err := range scanEvents(resp.Body) {
if err != nil {
return
}
select {
case s.incoming <- evt.Data:
case <-s.done:
return
}
}
}()
return s, nil
}
When the server sends an endpoint event whose data starts with / (for example /messages?sessionId=123), parsedURL.Parse(raw) replaces the entire path of c.Endpoint instead of preserving the prefix.
So if c.Endpoint is:
http://192.168.1.57:8000/mcp/http://192.168.1.30:8088/sse
and the server sends:
event: endpoint
data: /messages?sessionId=123
then msgEndpoint becomes:
http://192.168.1.57:8000/messages?sessionId=123
instead of:
http://192.168.1.57:8000/mcp/http://192.168.1.30:8088/messages?sessionId=123
This makes it impossible to use SSEClientTransport with an MCP server that is mounted behind a path prefix or with a reverse proxy that encodes routing information in the path.
This is very similar to the issue reported in the Python SDK in modelcontextprotocol/python-sdk#563.
To Reproduce
Steps to reproduce the behavior:
-
Put an MCP SSE server behind a reverse proxy that exposes it at a non-root path, for example:
http://192.168.1.57:8000/mcp/http://192.168.1.30:8088/sse -
Have the backend server send an
endpointevent like:event: endpoint data: /messages?sessionId=123 -
In a Go client, connect using
SSEClientTransport:transport := &mcp.SSEClientTransport{ Endpoint: "http://192.168.1.57:8000/mcp/http://192.168.1.30:8088/sse", } client := mcp.NewClient(&mcp.Implementation{ Name: "mcp-client", Version: "dev", }, nil) ctx := context.Background() session, err := client.Connect(ctx, transport, nil) if err != nil { log.Fatalf("Connect failed: %v", err) } defer session.Close() -
Check the HTTP traffic on
192.168.1.57:8000(e.g. reverse proxy logs). You’ll see:- The initial
GETgoes to/mcp/http://192.168.1.30:8088/sse(correct). - Subsequent POSTs go to
/messages?sessionId=123(missing the/mcp/http://192.168.1.30:8088prefix), which breaks any path-based routing.
- The initial
Expected behavior
The message endpoint resolution should not silently drop the original path prefix of c.Endpoint when the server sends the endpoint event.
For example, for:
Endpoint = "http://192.168.1.57:8000/mcp/http://192.168.1.30:8088/sse"endpointevent data/messages?sessionId=123
I would expect msgEndpoint to still include the prefix (or at least have a way to preserve it), so that path-based reverse proxies remain usable.
Logs
Example reverse-proxy logs (simplified):
# Initial SSE connection
192.168.1.100 - - "GET /mcp/http://192.168.1.30:8088/sse HTTP/1.1" 200 -
# Subsequent POST from go-sdk SSE client (missing prefix)
192.168.1.100 - - "POST /messages?sessionId=123 HTTP/1.1" 404 -
The proxy only routes /mcp/http://192.168.1.30:8088/..., so the POST to /messages fails.
Additional context
Two questions / possible directions where I’d really appreciate guidance from the maintainers:
-
Can / should servers return a “relative” path in the
endpointevent?
Right now many examples use a leading slash, e.g.data: /messages?sessionId=123.
If instead the server returned something likedata: messages?sessionId=123(no leading/), thenparsedURL.Parse(raw)would treat it as relative to the original endpoint path and the prefix would be preserved.- Is such a relative URI considered valid according to the MCP spec for
endpointevents? - If yes, should server implementations be encouraged to do this when they expect to live behind path prefixes?
- Is such a relative URI considered valid according to the MCP spec for
-
Would it make sense for the Go client to support configuring two paths / URLs?
For example:- One URL for the initial SSE
GET(Endpoint), and - Another explicit URL / base URL for the message POST endpoint (e.g.
MessageEndpointor similar), instead of always deriving it fromEndpoint+evt.Data.
This would make it much easier to support path-based reverse proxies (and setups where routing information is encoded in the path) without having to modify the MCP server itself.
- One URL for the initial SSE
Current workaround
As a temporary workaround, I’m using a rather ugly “double-prefixed” endpoint and a very permissive server handler:
-
Client-side
Endpointconfigured inSSEClientTransport:http://192.168.1.57:8000/mcp/http://192.168.1.30:8088/mcp/http://192.168.1.30:8088/
-
After the reverse proxy on
192.168.1.57:8000, this is forwarded to the backend as:http://192.168.1.30:8088/mcp/http://192.168.1.30:8088/ -
The MCP server (which also uses go-sdk) sends an
endpointevent like:event: endpoint data: /mcp/http://192.168.1.30:8088/?sessionid=xxx -
After the same proxy logic, the final POSTs end up at:
http://192.168.1.30:8088/?sessionid=xxx
On the server side I’m using the go-sdk SSE handler mounted at the root, without any path restrictions:
sseHandler := mcp.NewSSEHandler(s, nil)
err := http.ListenAndServe(address, sseHandler)
if !errors.Is(err, http.ErrServerClosed) {
slog.Error("failed to listen and serve", "error", err)
os.Exit(1)
}
Because NewSSEHandler is handling all incoming paths and only cares about the query parameter sessionid, this setup happens to work and the session ID is parsed correctly. However, it’s clearly a hacky workaround and illustrates how difficult it is to use SSEClientTransport cleanly when the server is behind a path-based reverse proxy.
- go-sdk version: v1.1.0
- Go version: 1.25.4
- OS: macOS
- Lenguaje dominante
- Go
- Estrellas
- 5.3k
- Forks
- 607
- Merge medio
- 1 d 11 h
- PR fusionados (30 d)
- 82
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la 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 modelcontextprotocol/go-sdk
-
Expose generic `SendNotification` on `ServerSession` for custom protocol extensionsQuizá libre de nuevo Un pull request para esta issue se cerró sin fusionarse. Abiertoneeds investigation
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
modelcontextprotocol/go-sdk#745 · 13 comentarios · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
mcp: whether MaxLineLength rejects a message depends on how the stream is readPosiblemente ocupada @Tangerg la tomó hoy. Abierto
modelcontextprotocol/go-sdk#1509 · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
Dificultad 3/5 1-2 días Aptitud para principiantes 62/100
modelcontextprotocol/go-sdk#1506 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
mcp: closing a session right after a cancelled call drops notifications/cancelled, so the server handler keeps runningPosiblemente ocupada @vshalpnjabi la tomó hoy. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
modelcontextprotocol/go-sdk#1505 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
mcp: a canceled write on IOTransport/StdioTransport cannot leave the write queuePosiblemente ocupada @Tangerg la tomó hace 1 día. Abierto
modelcontextprotocol/go-sdk#1498 · 1 comentario · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de modelcontextprotocol/go-sdk
Issues similares
-
Query cache: a failed cache page read leaves the set request's read lock held, so the scan hangsAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
turbot/steampipe-plugin-sdk#986 · 1 comentario ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
router-for-me/CLIProxyAPI#7307 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
betterleaks/betterleaks#460 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 79/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
openmcp-project/service-provider-crossplane#330 ·
Los mantenedores suelen responder en 1 día