diff --git a/internal/mcp/http.go b/internal/mcp/http.go index fdbbe33711b5bbbf6d0f82b152ad2a89bd226188..f3864818cc1968313a80c85c0f5b69e0a5e8ef7d 100644 --- a/internal/mcp/http.go +++ b/internal/mcp/http.go @@ -8,8 +8,26 @@ import ( "crypto/subtle" "net/http" "strings" + + sdk "github.com/modelcontextprotocol/go-sdk/mcp" ) +// HTTPHandler returns the authenticated Streamable HTTP MCP handler. +func (s *Server) HTTPHandler(token string) http.Handler { + streamable := sdk.NewStreamableHTTPHandler(func(*http.Request) *sdk.Server { + return s.sdk + }, nil) + + return newHTTPHandler(token, streamable) +} + +func newHTTPHandler(token string, mcpHandler http.Handler) http.Handler { + mux := http.NewServeMux() + mux.Handle("/mcp", requireBearerToken(token, mcpHandler)) + + return mux +} + func requireBearerToken(token string, next http.Handler) http.Handler { if token == "" { return next diff --git a/internal/mcp/http_test.go b/internal/mcp/http_test.go index 3f4ab4214ea148de91c6b4fd6d1ad6a313f2b3e2..d323f31cd90104cc1c2869737e50a9497f82932d 100644 --- a/internal/mcp/http_test.go +++ b/internal/mcp/http_test.go @@ -12,6 +12,96 @@ import ( "testing" ) +// server.TRANSPORT.5 +func TestHTTPHandlerACIDServerTransport5RoutesMCPPath(t *testing.T) { + calls := 0 + handler := newHTTPHandler("", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls++ + w.WriteHeader(http.StatusNoContent) + })) + + recorder := httptest.NewRecorder() + request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil) + handler.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusNoContent { + t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNoContent) + } + if calls != 1 { + t.Fatalf("MCP handler calls = %d, want 1", calls) + } +} + +// server.TRANSPORT.5 +func TestHTTPHandlerACIDServerTransport5DoesNotRouteOtherPaths(t *testing.T) { + tests := []string{"/", "/mcp/extra"} + + for _, path := range tests { + t.Run(path, func(t *testing.T) { + calls := 0 + handler := newHTTPHandler("", http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + calls++ + })) + + recorder := httptest.NewRecorder() + request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, path, nil) + handler.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusNotFound { + t.Fatalf("status = %d, want %d", recorder.Code, http.StatusNotFound) + } + if calls != 0 { + t.Fatalf("MCP handler calls = %d, want 0", calls) + } + }) + } +} + +// server.SECURITY.7 +func TestHTTPHandlerACIDServerSecurity7GatesMCPPathWithBearerToken(t *testing.T) { + calls := 0 + handler := newHTTPHandler("secret-token", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls++ + w.WriteHeader(http.StatusNoContent) + })) + + unauthorized := httptest.NewRecorder() + unauthorizedRequest := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil) + handler.ServeHTTP(unauthorized, unauthorizedRequest) + + if unauthorized.Code != http.StatusUnauthorized { + t.Fatalf("unauthorized status = %d, want %d", unauthorized.Code, http.StatusUnauthorized) + } + if calls != 0 { + t.Fatalf("MCP handler calls after unauthorized request = %d, want 0", calls) + } + + authorized := httptest.NewRecorder() + authorizedRequest := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil) + authorizedRequest.Header.Set("Authorization", "Bearer secret-token") + handler.ServeHTTP(authorized, authorizedRequest) + + if authorized.Code != http.StatusNoContent { + t.Fatalf("authorized status = %d, want %d", authorized.Code, http.StatusNoContent) + } + if calls != 1 { + t.Fatalf("MCP handler calls after authorized request = %d, want 1", calls) + } +} + +// server.SECURITY.7 +func TestServerHTTPHandlerACIDServerSecurity7WrapsSDKHandlerWithBearerAuth(t *testing.T) { + handler := NewServer(nil, "test").HTTPHandler("secret-token") + + recorder := httptest.NewRecorder() + request := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/mcp", nil) + handler.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want %d", recorder.Code, http.StatusUnauthorized) + } +} + // server.SECURITY.3 server.SECURITY.6 func TestHTTPBearerAuthACIDServerSecurity3And6AllowsMatchingBearerToken(t *testing.T) { calls := 0