- store_redis.go: records with PX TTLs, per-binding sets, pub/sub events, opened lazily from redis.url under cooler's prefix (nothing dials at mount) - events.go: pact.MCPSubjectChanged republished through the store; every instance recomputes the subject's sessions and stateless servers (list_changed) or ends them on revocation; the link cache is dropped - stateless.go: the SDK's stateless handler over per-identity servers for 2026-07-28 clients, routed by the protocol header or server/discover - prm.go: RFC 9728 metadata at both well-known paths; long GET streams re-verify their bearer every minute - TestPierSessionsSmoke, TestPierFanOutSmoke, TestPierStatelessSmoke on miniredis; README features, configuration and operational notes
81 lines
2.7 KiB
Go
81 lines
2.7 KiB
Go
package pier_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// Smoke test of phase 19.7 plan 02 Task 3: the hybrid protocol (D-32). A
|
|
// client at the SDK's newest protocol negotiates 2026-07-28 statelessly
|
|
// (no Mcp-Session-Id) and calls the tool; a client pinned to 2025-11-25
|
|
// gets a session on the stateful path; both see the same per-identity list.
|
|
func TestPierStatelessSmoke(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
app := pierApp(t, "enabled: false\n")
|
|
host, err := mountPier(t, app, []party.Plugin{acmePlugin{tools: []pact.MCPTool{echoTool()}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := httptest.NewServer(host)
|
|
defer srv.Close()
|
|
transport := func() *mcp.StreamableClientTransport {
|
|
return &mcp.StreamableClientTransport{
|
|
Endpoint: srv.URL + "/mcp",
|
|
HTTPClient: &http.Client{Transport: bearerTransport{token: "oas_at_test"}},
|
|
}
|
|
}
|
|
|
|
newest := mcp.NewClient(&mcp.Implementation{Name: "newest", Version: "1"}, nil)
|
|
stateless, err := newest.Connect(ctx, transport(), nil)
|
|
if err != nil {
|
|
t.Fatalf("stateless connect: %v", err)
|
|
}
|
|
defer stateless.Close()
|
|
if stateless.ID() != "" {
|
|
t.Fatalf("the newest client got a session id %q, want none", stateless.ID())
|
|
}
|
|
if v := stateless.InitializeResult().ProtocolVersion; v != "2026-07-28" {
|
|
t.Fatalf("negotiated %q, want 2026-07-28", v)
|
|
}
|
|
res, err := stateless.CallTool(ctx, &mcp.CallToolParams{Name: "acme_echo", Arguments: map[string]any{"id": 3, "text": "yo"}})
|
|
if err != nil || res.IsError {
|
|
t.Fatalf("stateless tools/call = %+v, %v", res, err)
|
|
}
|
|
if structured, _ := res.StructuredContent.(map[string]any); structured["principal"] != 7.0 || structured["id"] != 3.0 {
|
|
t.Fatalf("stateless structuredContent = %+v", res.StructuredContent)
|
|
}
|
|
|
|
legacy := mcp.NewClient(&mcp.Implementation{Name: "legacy", Version: "1"}, nil)
|
|
stateful, err := legacy.Connect(ctx, transport(), &mcp.ClientSessionOptions{ProtocolVersion: "2025-11-25"})
|
|
if err != nil {
|
|
t.Fatalf("stateful connect: %v", err)
|
|
}
|
|
defer stateful.Close()
|
|
if stateful.ID() == "" {
|
|
t.Fatal("the 2025-11-25 client got no session id")
|
|
}
|
|
if v := stateful.InitializeResult().ProtocolVersion; v != "2025-11-25" {
|
|
t.Fatalf("negotiated %q, want 2025-11-25", v)
|
|
}
|
|
|
|
a, err := stateless.ListTools(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := stateful.ListTools(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(a.Tools) != 1 || len(b.Tools) != 1 || a.Tools[0].Name != b.Tools[0].Name || a.Tools[0].Name != "acme_echo" {
|
|
t.Fatalf("lists differ: stateless %+v, stateful %+v", a.Tools, b.Tools)
|
|
}
|
|
}
|