package pier_test import ( "bytes" "context" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "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). 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) }