package pier_test import ( "context" "encoding/json" "net/http" "testing" "time" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/pier" "github.com/modelcontextprotocol/go-sdk/mcp" ) // TestDispatchMCPCallBothGuards pins D-11: one frontend tool call and one // backend tool call each carry MCPCall for the guard that owns the tool. // SessionID is the stateful session, never the word ephemeral. A // personal-token versus OAuth difference is not asserted here. func TestDispatchMCPCallBothGuards(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() links := newFakeLinks() links.set(10, consentedAdmin) front := &fakeGuard{} back := &fakeGuard{name: pier.BackendGuard, tokenMW: pier.BackendGuard} verifier := newFakeVerifier(map[string]pact.MCPGrant{ "oas_at_admin": grant(10, "c4", 4, "read", "write", "backend"), }) app := newApp(t, appSpec{ guards: []pact.MCPGuard{front, back}, verifier: verifier, links: links, }) acme := routePlugin{ id: "test.acme", routes: []testRoute{{ method: http.MethodGet, path: "/acme/ping", mw: []string{"acme.auth"}, h: mcpCallHandler, }}, tools: []pact.MCPTool{{ Name: "ping", Method: http.MethodGet, Path: "/acme/ping", Input: struct{}{}, }}, } office := routePlugin{ id: "test.office", guard: pier.BackendGuard, middleware: pier.BackendGuard, routes: []testRoute{{ method: http.MethodGet, path: "/office/ping", mw: []string{pier.BackendGuard}, h: mcpCallHandler, }}, tools: []pact.MCPTool{{ Name: "ping", Method: http.MethodGet, Path: "/office/ping", Input: struct{}{}, }}, } in := startInstance(t, app, acme, office) cs := sdkClient(t, ctx, in.url(), "oas_at_admin", clientOpts{}) frontBody := callMCP(t, ctx, cs, "acme_ping") if frontBody["ok"] != true || frontBody["guard"] != "acme" || frontBody["tool"] != "acme_ping" { t.Fatalf("frontend MCPCall = %+v, want guard acme tool acme_ping", frontBody) } if session, _ := frontBody["session"].(string); session == "" || session == "ephemeral" { t.Fatalf("frontend session = %q, want the stateful session id", session) } backBody := callMCP(t, ctx, cs, "office_ping") if backBody["ok"] != true || backBody["guard"] != pier.BackendGuard || backBody["tool"] != "office_ping" { t.Fatalf("backend MCPCall = %+v, want guard %s tool office_ping", backBody, pier.BackendGuard) } if session, _ := backBody["session"].(string); session == "" || session == "ephemeral" { t.Fatalf("backend session = %q, want the stateful session id", session) } } func mcpCallHandler(w http.ResponseWriter, r *http.Request) { call, ok := pact.MCPCallFrom(r.Context()) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "ok": ok, "guard": call.Subject.Guard, "tool": call.Tool, "session": call.SessionID, }) } func callMCP(t *testing.T, ctx context.Context, cs *mcp.ClientSession, name string) map[string]any { t.Helper() res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: name}) if err != nil || res.IsError { t.Fatalf("tools/call %s: %+v, %v", name, res, err) } return structured(t, res) }