diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 7db44d9..71cbba0 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -56,7 +56,6 @@ func (s *Server) handlePOST(w http.ResponseWriter, r *http.Request) { return } - // initialize is the only method allowed without a session. if req.Method == "initialize" { sid := s.opts.Sessions.Issue() w.Header().Set("Mcp-Session-Id", sid) @@ -68,11 +67,12 @@ func (s *Server) handlePOST(w http.ResponseWriter, r *http.Request) { return } - sid := r.Header.Get("Mcp-Session-Id") - if !s.opts.Sessions.Valid(sid) { - http.Error(w, "missing or invalid Mcp-Session-Id", http.StatusBadRequest) - return - } + // Mcp-Session-Id is advisory: we issue one on initialize and accept it back, + // but every tool the gitea-mcp server exposes is stateless single-shot, so + // we do not gate non-initialize calls on it. The claude.ai connector's + // 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 { case "tools/list": diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 15f15fe..f6809d0 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -57,14 +57,22 @@ func TestInitialize(t *testing.T) { 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) rr := postJSON(t, srv, map[string]any{ "jsonrpc": "2.0", "id": 2, "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) {