fix(mcp): drop strict session-id requirement on POST /mcp
The claude.ai connector's MCP transport proxy does not reliably
propagate the Mcp-Session-Id header issued during initialize. With the
previous strict gate (return 400 plain text "missing or invalid
Mcp-Session-Id"), every tools/list and tools/call from claude.ai
failed and the Anthropic proxy surfaced it as:
Streamable HTTP error: {"jsonrpc":"2.0","id":N,"error":
{"code":-32600,"message":"Anthropic Proxy: Invalid content from server"}}
— because the plain-text 400 response is not valid JSON-RPC.
All tools the gitea-mcp server exposes are stateless single-shot
calls, so there is no functional reason to gate them on a session.
brain-mcp and supervisor-mcp don't gate either, and claude.ai works
against them fine. Match that behavior: keep issuing Mcp-Session-Id
on initialize for clients that want to use it, but stop rejecting
calls that don't send one back.
Test renamed PostWithoutSessionRejected → PostWithoutSessionAccepted
and updated to assert the tools/list response shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,6 @@ func (s *Server) handlePOST(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize is the only method allowed without a session.
|
|
||||||
if req.Method == "initialize" {
|
if req.Method == "initialize" {
|
||||||
sid := s.opts.Sessions.Issue()
|
sid := s.opts.Sessions.Issue()
|
||||||
w.Header().Set("Mcp-Session-Id", sid)
|
w.Header().Set("Mcp-Session-Id", sid)
|
||||||
@@ -68,11 +67,12 @@ func (s *Server) handlePOST(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sid := r.Header.Get("Mcp-Session-Id")
|
// Mcp-Session-Id is advisory: we issue one on initialize and accept it back,
|
||||||
if !s.opts.Sessions.Valid(sid) {
|
// but every tool the gitea-mcp server exposes is stateless single-shot, so
|
||||||
http.Error(w, "missing or invalid Mcp-Session-Id", http.StatusBadRequest)
|
// we do not gate non-initialize calls on it. The claude.ai connector's
|
||||||
return
|
// transport proxy is observed to not propagate the session header reliably,
|
||||||
}
|
// and the spec allows servers to be sessionless. Compare with brain-mcp /
|
||||||
|
// supervisor-mcp, which never required a session at all.
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "tools/list":
|
case "tools/list":
|
||||||
|
|||||||
@@ -57,14 +57,22 @@ func TestInitialize(t *testing.T) {
|
|||||||
assert.Equal(t, "gitea-mcp", si["name"])
|
assert.Equal(t, "gitea-mcp", si["name"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostWithoutSessionRejected(t *testing.T) {
|
func TestPostWithoutSessionAccepted(t *testing.T) {
|
||||||
|
// gitea-mcp tools are stateless single-shot; Mcp-Session-Id is advisory.
|
||||||
|
// claude.ai's MCP transport proxy is observed to not propagate the
|
||||||
|
// session header reliably, so non-initialize calls must work without it.
|
||||||
srv := newServer(t)
|
srv := newServer(t)
|
||||||
rr := postJSON(t, srv, map[string]any{
|
rr := postJSON(t, srv, map[string]any{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"method": "tools/list",
|
"method": "tools/list",
|
||||||
}, "")
|
}, "")
|
||||||
require.Equal(t, http.StatusBadRequest, rr.Code)
|
require.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
||||||
|
result := resp["result"].(map[string]any)
|
||||||
|
assert.Contains(t, result, "tools")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerWithOriginAllowlistRejectsBadOrigin(t *testing.T) {
|
func TestServerWithOriginAllowlistRejectsBadOrigin(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user