@@ -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
@@ -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