package pier_test import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strconv" "strings" "sync/atomic" "testing" "time" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/pier" "git.golem15.com/golem15/summercms/modules/surf" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Tracer smoke test of phase 19.7 plan 02 (Task 2): one tool declared by a // fake plugin is listed and called end to end by the SDK client through // pier's stateful path, dispatching in-process through surf's compiled // guard chain with the bouncer dispatch marker. // // The test lives in package pier_test and builds a real surf router with // surf.BuildRouter, then mounts pier through a tiny in-test Host adapter // that records the endpoint handler instead of compiling a route, so the // pier package tests need no build tag (with the tag, surf imports pier, // which an external test package may still import). // refusingGuard is the fake plugin's bouncer guard: it never authenticates // an HTTP request and writes its own 401, so only the dispatch marker can // get a request through the route. type refusingGuard struct{} func (refusingGuard) AuthenticateCredential(*http.Request) (*bouncer.Principal, any, error) { return nil, nil, errors.New("acme: no token") } func (refusingGuard) WriteUnauthorized(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":true,"message":"Unauthenticated"}`)) } // echoInput is the tool's input: id fills {id}, text forms the body. type echoInput struct { ID int `json:"id" path:"id" jsonschema:"the record id"` Text string `json:"text" jsonschema:"the text to echo"` } // acmePlugin registers bouncer guard "acme" as middleware "acme.auth", one // guarded route and the MCP tools it is given. type acmePlugin struct { tools []pact.MCPTool prompts []pact.MCPPrompt } func (acmePlugin) ID() string { return "test.acme" } func (acmePlugin) Requires() []string { return nil } func (acmePlugin) Register(*backpack.App) error { return nil } func (acmePlugin) Boot(*backpack.App) error { return nil } func (p acmePlugin) Middlewares() map[string]pact.Middleware { guards := bouncer.NewRegistry() if err := guards.Register(p.ID(), "acme", refusingGuard{}); err != nil { panic(err) } auth, err := guards.Middleware("acme") if err != nil { panic(err) } return map[string]pact.Middleware{"acme.auth": auth} } func (acmePlugin) Routes(r pact.Router) error { r.Group("/acme", []string{"acme.auth"}, func(g pact.Router) { g.Get("/items/{id}", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"item":"` + req.PathValue("id") + `"}`)) }) g.Post("/echo/{id}", func(w http.ResponseWriter, req *http.Request) { user, _ := bouncer.User(req.Context()) var body map[string]any _ = json.NewDecoder(req.Body).Decode(&body) id, _ := strconv.Atoi(req.PathValue("id")) _, cookieErr := req.Cookie("summer_session") hasCookie := cookieErr == nil w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "id": id, "body": body, "principal": user.ID, "auth": req.Header.Get("Authorization") != "" || hasCookie, }) }) }) return nil } func (p acmePlugin) MCP() pact.MCP { return pact.MCP{Tools: p.tools, Prompts: p.prompts} } // acmeGuard is the fake guard provider for "acme": token-guarded by // acme.auth, allows everything unless deny is set, dispatches as the // subject's user id. type acmeGuard struct { deny atomic.Bool } func (*acmeGuard) MCPGuardName() string { return "acme" } func (*acmeGuard) MCPTokenGuarded(mw []string) bool { for _, m := range mw { if m == "acme.auth" { return true } } return false } func (*acmeGuard) MCPRequirements([]string) (pact.MCPRequirements, error) { return pact.MCPRequirements{}, nil } func (g *acmeGuard) MCPAllows(context.Context, pact.MCPSubject, pact.MCPRequirements, []string) (bool, error) { return !g.deny.Load(), nil } func (*acmeGuard) MCPDispatch(_ context.Context, subject pact.MCPSubject, _ string) (pact.MCPDispatch, error) { return pact.MCPDispatch{Principal: &bouncer.Principal{ID: subject.UserID}, Guards: []string{"acme"}}, nil } // acmeVerifier accepts oas_at_test for user 7, client c1, read and write, // and oas_at_other for user 8, client c2 (another binding). type acmeVerifier struct{} func (acmeVerifier) MCPVerifyToken(_ context.Context, raw string) (pact.MCPGrant, error) { switch raw { case "oas_at_test": return pact.MCPGrant{UserID: 7, ClientID: "c1", TokenID: 1, Scopes: []string{"read", "write"}, ExpiresAt: time.Now().Add(time.Hour)}, nil case "oas_at_other": return pact.MCPGrant{UserID: 8, ClientID: "c2", TokenID: 2, Scopes: []string{"read", "write"}, ExpiresAt: time.Now().Add(time.Hour)}, nil case "oas_at_read": return pact.MCPGrant{UserID: 9, ClientID: "c3", TokenID: 3, Scopes: []string{"read"}, ExpiresAt: time.Now().Add(time.Hour)}, nil } return pact.MCPGrant{}, pact.ErrMCPTokenInvalid } func (acmeVerifier) MCPResource() string { return "http://acme.test/mcp" } // testHost adapts a built surf router to pier.Host and records the // endpoint handler pier mounts instead of compiling a route. type testHost struct { r *surf.Router handlers map[string]http.Handler path string pier *pier.Pier } func (h *testHost) Routes() []pier.RouteInfo { var out []pier.RouteInfo for _, ri := range h.r.Routes() { out = append(out, pier.RouteInfo{Method: ri.Method, Pattern: ri.Pattern, PluginID: ri.PluginID, Middleware: ri.Middleware}) } return out } func (h *testHost) Handler(method, pattern string) (http.Handler, bool) { return h.r.Handler(method, pattern) } func (h *testHost) MountStream(method, path string, handler http.Handler, _ ...string) error { h.handlers[method] = handler h.path = path return nil } func (h *testHost) MountJSON(method, path string, handler http.Handler) error { h.handlers[method+" "+path] = handler return nil } func (h *testHost) Owner(method, path string) (string, bool) { for _, ri := range h.r.Routes() { if ri.Method == method && ri.Pattern == path { return ri.PluginID, true } } return "", false } // ServeHTTP routes the endpoint path to the recorded handler of the // request's method and any other recorded route by method and path. func (h *testHost) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == h.path { if handler, ok := h.handlers[r.Method]; ok { handler.ServeHTTP(w, r) return } } if handler, ok := h.handlers[r.Method+" "+r.URL.Path]; ok { handler.ServeHTTP(w, r) return } http.NotFound(w, r) } // pierApp builds an app with mcp.yaml from the given body and publishes // the fake verifier and a guard registry with a fresh acme guard. func pierApp(t *testing.T, mcpYAML string) *backpack.App { t.Helper() return pierAppWith(t, map[string]string{"mcp.yaml": mcpYAML}, &acmeGuard{}) } // pierAppWith builds an app from extra config files (app.yaml and // http.yaml have defaults) and publishes the fake verifier and, unless // guard is nil, a guard registry with that provider; the router is built // with the MCP flag off (so no tag is needed) and the flag is enabled for // pier.Mount afterwards. func pierAppWith(t *testing.T, extra map[string]string, guard *acmeGuard) *backpack.App { t.Helper() dir := t.TempDir() files := map[string]string{ "app.yaml": "name: acme\nurl: http://acme.test\n", "http.yaml": "body_limits:\n default_bytes: 1048576\n upload_bytes: 1048576\n", } for name, body := range extra { files[name] = body } for name, body := range files { if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{"SUMMER_ENV=development"}}) if err != nil { t.Fatal(err) } app := backpack.New(cfg) if err := app.Publish[pact.MCPTokenVerifier](acmeVerifier{}); err != nil { t.Fatal(err) } if guard == nil { return app } reg := pact.NewMCPGuards() if err := reg.Register(guard); err != nil { t.Fatal(err) } if err := app.Publish(reg); err != nil { t.Fatal(err) } return app } // mountPier builds the router for plugins with the flag off, then mounts // pier with it on, through the test host. func mountPier(t *testing.T, app *backpack.App, plugins []party.Plugin) (*testHost, error) { t.Helper() router, err := surf.BuildRouter(app, plugins) if err != nil { t.Fatal(err) } if err := app.Config.Set("mcp.enabled", true); err != nil { t.Fatal(err) } host := &testHost{r: router, handlers: map[string]http.Handler{}} err = pier.Mount(host, app, plugins) if err == nil { host.pier, _ = app.Lookup[*pier.Pier]() } return host, err } // bearerTransport adds the bearer to every request of the SDK client. type bearerTransport struct { token string } func (b bearerTransport) RoundTrip(r *http.Request) (*http.Response, error) { r = r.Clone(r.Context()) r.Header.Set("Authorization", "Bearer "+b.token) return http.DefaultTransport.RoundTrip(r) } func echoTool() pact.MCPTool { return pact.MCPTool{ Name: "echo", Title: "Echo", Description: "Echoes the text back with the record id and the acting principal.", Method: "POST", Path: "/acme/echo/{id}", Input: echoInput{}, } } func TestPierTracerSmoke(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() app := pierApp(t, "enabled: false\n") plugins := []party.Plugin{acmePlugin{tools: []pact.MCPTool{echoTool()}}} host, err := mountPier(t, app, plugins) if err != nil { t.Fatal(err) } for _, m := range []string{"POST", "GET", "DELETE"} { if host.handlers[m] == nil { t.Fatalf("pier did not mount %s %s", m, host.path) } } srv := httptest.NewServer(host) defer srv.Close() client := mcp.NewClient(&mcp.Implementation{Name: "acme-test", Version: "1"}, nil) transport := &mcp.StreamableClientTransport{ Endpoint: srv.URL + "/mcp", HTTPClient: &http.Client{Transport: bearerTransport{token: "oas_at_test"}}, } // Pinned to 2025-11-25: the stateful path with sessions (the newest // protocol is served statelessly, see TestPierStatelessSmoke). cs, err := client.Connect(ctx, transport, &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"}) if err != nil { t.Fatalf("connect: %v", err) } defer cs.Close() if cs.ID() == "" { t.Fatal("the stateful path answered no Mcp-Session-Id") } list, err := cs.ListTools(ctx, nil) if err != nil { t.Fatalf("tools/list: %v", err) } if len(list.Tools) != 1 || list.Tools[0].Name != "acme_echo" { t.Fatalf("tools/list = %+v, want exactly acme_echo", list.Tools) } ann := list.Tools[0].Annotations if ann == nil || ann.ReadOnlyHint || ann.DestructiveHint == nil || *ann.DestructiveHint { t.Fatalf("annotations = %+v, want readOnlyHint false and destructiveHint false", ann) } res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "acme_echo", Arguments: map[string]any{"id": 5, "text": "hi"}}) if err != nil { t.Fatalf("tools/call: %v", err) } if res.IsError { t.Fatalf("tools/call isError: %+v", res.Content) } structured, _ := res.StructuredContent.(map[string]any) if structured["principal"] != 7.0 || structured["id"] != 5.0 || structured["auth"] != false { t.Fatalf("structuredContent = %+v, want principal 7, id 5, auth false", structured) } body, _ := structured["body"].(map[string]any) if len(body) != 1 || body["text"] != "hi" { t.Fatalf("body = %+v, want {text: hi} without id", body) } if len(res.Content) != 1 { t.Fatalf("content = %+v, want one text item", res.Content) } if text, ok := res.Content[0].(*mcp.TextContent); !ok || !strings.Contains(text.Text, `"principal":7`) { t.Fatalf("text content = %+v", res.Content[0]) } // A bad bearer is a 401 carrying the protected resource metadata challenge. init := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"x","version":"1"}}}` req, _ := http.NewRequestWithContext(ctx, http.MethodPost, srv.URL+"/mcp", bytes.NewBufferString(init)) req.Header.Set("Authorization", "Bearer oas_at_bad") req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() challenge := resp.Header.Get("WWW-Authenticate") if resp.StatusCode != http.StatusUnauthorized || !strings.Contains(challenge, `resource_metadata="http://acme.test/.well-known/oauth-protected-resource/mcp"`) || !strings.Contains(challenge, `scope="read"`) { t.Fatalf("bad bearer = %d %q", resp.StatusCode, challenge) } // The same tool declared twice fails Mount naming the exposed name. twice := []party.Plugin{acmePlugin{tools: []pact.MCPTool{echoTool(), echoTool()}}} if _, err := mountPier(t, pierApp(t, "enabled: false\n"), twice); err == nil || !strings.Contains(err.Error(), "acme_echo") { t.Fatalf("duplicate tool: err = %v, want one naming acme_echo", err) } // A tool bound to a route nobody registers fails Mount naming the route. nope := echoTool() nope.Name, nope.Method, nope.Path = "nope", "GET", "/nope" nope.Input = struct{}{} missing := []party.Plugin{acmePlugin{tools: []pact.MCPTool{nope}}} if _, err := mountPier(t, pierApp(t, "enabled: false\n"), missing); err == nil || !strings.Contains(err.Error(), "/nope") { t.Fatalf("missing route: err = %v, want one naming /nope", err) } // A plugin that declares nothing exposes nothing, and mcp.enabled false mounts nothing. none, err := mountPier(t, pierApp(t, "enabled: false\n"), []party.Plugin{acmePlugin{}}) if err != nil || none.handlers["POST"] == nil { t.Fatalf("plugin without tools: err = %v, mounted = %v", err, none.handlers["POST"] != nil) } off := pierApp(t, "enabled: false\n") router, err := surf.BuildRouter(off, plugins) if err != nil { t.Fatal(err) } offHost := &testHost{r: router, handlers: map[string]http.Handler{}} if err := pier.Mount(offHost, off, plugins); err != nil || len(offHost.handlers) != 0 { t.Fatalf("mcp.enabled false: err = %v, handlers = %d", err, len(offHost.handlers)) } tracerIdentityCases(t, ctx) }