feat(mcp): streamable HTTP transport with session, init, and dispatch
Implements the Streamable HTTP transport: POST routing handles initialize (issues session ID), tools/list, tools/call, and unknown methods; GET SSE emits a keepalive comment then blocks on context cancellation. A minimal registry stub is introduced so the server compiles and tools/list returns an empty array until Phase 6+ registers real tools. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
92
internal/mcp/server_test.go
Normal file
92
internal/mcp/server_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package mcp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.d-ma.be/mathias/gitea-mcp/internal/mcp"
|
||||
"gitea.d-ma.be/mathias/gitea-mcp/internal/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newServer(t *testing.T) *mcp.Server {
|
||||
t.Helper()
|
||||
reg := registry.New()
|
||||
return mcp.NewServer(mcp.ServerOptions{
|
||||
Registry: reg,
|
||||
OriginAllowlist: nil,
|
||||
Sessions: mcp.NewSessionStore(),
|
||||
})
|
||||
}
|
||||
|
||||
func postJSON(t *testing.T, srv http.Handler, body any, sessionID string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
b, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(b))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json, text/event-stream")
|
||||
if sessionID != "" {
|
||||
req.Header.Set("Mcp-Session-Id", sessionID)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
func TestInitialize(t *testing.T) {
|
||||
srv := newServer(t)
|
||||
rr := postJSON(t, srv, map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": map[string]any{"protocolVersion": "2025-06-18"},
|
||||
}, "")
|
||||
require.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
sid := rr.Header().Get("Mcp-Session-Id")
|
||||
assert.NotEmpty(t, sid)
|
||||
|
||||
var resp map[string]any
|
||||
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
||||
result := resp["result"].(map[string]any)
|
||||
assert.Equal(t, "2025-06-18", result["protocolVersion"])
|
||||
si := result["serverInfo"].(map[string]any)
|
||||
assert.Equal(t, "gitea-mcp", si["name"])
|
||||
}
|
||||
|
||||
func TestPostWithoutSessionRejected(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestToolsListAfterInitialize(t *testing.T) {
|
||||
srv := newServer(t)
|
||||
init := postJSON(t, srv, map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": map[string]any{"protocolVersion": "2025-06-18"},
|
||||
}, "")
|
||||
sid := init.Header().Get("Mcp-Session-Id")
|
||||
|
||||
rr := postJSON(t, srv, map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/list",
|
||||
}, sid)
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user