diff --git a/docs/superpowers/plans/2026-09-29-go-mcp-timestamping-server.md b/docs/superpowers/plans/2026-09-29-go-mcp-timestamping-server.md new file mode 100644 index 0000000..f74d415 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-go-mcp-timestamping-server.md @@ -0,0 +1,245 @@ +# Go MCP Timestamping Server + pkg/tsnats Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship a Go MCP timestamping server (full tool parity with Python) plus importable `pkg/tsnats` for bulk NATS clients, with shadow deploy docs for ns1 `:8001`. + +**Architecture:** Official `go-sdk/mcp` for AI-facing Streamable HTTP/stdio; FileLedger + VeraeTime HTTPS; optional NATS upstream via `pkg/tsnats`. Job-complete stays core NATS only (no JetStream). + +**Tech Stack:** Go 1.22+, `github.com/modelcontextprotocol/go-sdk`, `github.com/nats-io/nats.go`, `net/http`, stdio. + +**Spec:** `docs/superpowers/specs/2026-09-29-go-mcp-timestamping-server-design.md` + +## Global Constraints + +- Digests only: refuse content/bytes/file/data/path/body/payload/blob/archive. +- Global stamp `restamp: false` by default; poll `/api/status/{jobId}` when needed. +- Never log JWT or password values. +- No JetStream (or other persistent stream) for job-complete / jobId notification. +- MCP tools: `stamp`, `verify`, `hash_report`, `signup`, `login`, `session`, `create_user`. +- `pkg/tsnats` subjects: `verae.ts.request`, `verae.ts.verify`, `verae.hash.report`; subscribe `verae.ts.completed`. +- Shadow default port `8001`; Python keeps `8000`. +- Work in `/Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server-go`. +- READMEs follow Standard Readme when touched. +- Frequent commits; one logical change each. + +## File map + +| Path | Responsibility | +| --- | --- | +| `go.mod` / `go.sum` | Module + deps | +| `cmd/verae-mcp-timestamping-server/main.go` | Flags, env, serve | +| `internal/digest/` | Normalize SHA-256 + forbidden keys | +| `internal/config/` | Env loading | +| `pkg/tsnats/` | Importable NATS client | +| `internal/veraetime/` | Global HTTPS + auth.properties | +| `internal/ledger/` | FileLedger local/shared | +| `internal/natsbridge/` | MCP upstream wrapping tsnats | +| `internal/tools/` | MCP tool registration/handlers | +| `internal/server/` | MCP server wiring (HTTP + stdio) | +| `docs/cutover.md` | Shadow + cutover checklist | +| `README.md` | Standard Readme | + +--- + +### Task 1: Module scaffold + digest package + +**Files:** +- Create: `go.mod`, `internal/digest/digest.go`, `internal/digest/digest_test.go`, `.gitignore` +- Modify: `README.md` (Install already present; keep Standard Readme) + +**Interfaces:** +- Produces: `NormalizeSHA256(s string) (string, error)`, `AssertDigestOnly(args map[string]any) error`, `ForbiddenKeys` + +- [ ] **Step 1: Write failing tests** + +```go +// internal/digest/digest_test.go +func TestNormalizeSHA256_StripPrefix(t *testing.T) { + h := strings.Repeat("a", 64) + got, err := NormalizeSHA256("sha256:" + h) + if err != nil || got != h { + t.Fatalf("got %q err %v", got, err) + } +} +func TestAssertDigestOnly_RejectsContent(t *testing.T) { + err := AssertDigestOnly(map[string]any{"sha256": strings.Repeat("a", 64), "content": "x"}) + if err == nil { + t.Fatal("expected error") + } +} +``` + +- [ ] **Step 2: Run tests — expect fail** + +Run: `go test ./internal/digest/ -count=1` +Expected: build/package errors + +- [ ] **Step 3: Implement digest + `go mod init`** + +```sh +go mod init git.georgelambert.org/marchon/verae-mcp-timestamping-server-go +``` + +Implement normalize (lower hex, strip `sha256:`) and forbidden-key check. Add `.gitignore` for `bin/`, `*.exe`, `.venv/`. + +- [ ] **Step 4: Tests pass** + +Run: `go test ./internal/digest/ -count=1` + +- [ ] **Step 5: Commit** + +```bash +git add go.mod internal/digest .gitignore +git commit -m "feat: module scaffold and digest normalize/guard" +``` + +--- + +### Task 2: pkg/tsnats client + +**Files:** +- Create: `pkg/tsnats/client.go`, `pkg/tsnats/client_test.go`, `pkg/tsnats/subjects.go` + +**Interfaces:** +- Consumes: `internal/digest` (or duplicate thin normalize in pkg to avoid internal import — **Ruling: put a small `NormalizeSHA256` copy in `pkg/tsnats` or export via a public `pkg/digest`; prefer `pkg/digest` moved from internal if needed**) +- Produces: + - `type Client struct` + - `Connect(url string, opts ...Option) (*Client, error)` + - `Stamp(ctx, sha256 string, opts StampOptions) ([]Receipt, error)` → `verae.ts.request` + - `Verify(ctx, receiptID string, opts VerifyOptions) (VerifyResult, error)` + - `HashReport(ctx, sha256 string, opts ReportOptions) ([]Receipt, error)` + - `SubscribeCompleted(ctx, func(CompletedEvent)) (Subscription, error)` → core subscribe `verae.ts.completed` + +**Ruling for Task 2:** Move digest helpers to `pkg/digest` so both `pkg/tsnats` and `internal/` can import them (Go forbids `internal/` import from `pkg/`). Update Task 1 packages in the same commit if still only local, or do a quick move commit first. + +- [ ] **Step 1: Write failing client tests with nats-server or mock Conn** + +Prefer `natsserver.RunServer` from `github.com/nats-io/nats-server/v2/test` or inject a `Requester` interface: + +```go +type Requester interface { + Request(ctx context.Context, subj string, data []byte) ([]byte, error) +} +``` + +Test: Stamp sends JSON `{"hash":"...","scope":"global"}` on `verae.ts.request` and parses receipt list. + +- [ ] **Step 2: Run — expect fail** + +- [ ] **Step 3: Implement subjects + Client** + +Subjects constants exactly: `verae.ts.request`, `verae.ts.verify`, `verae.hash.report`, `verae.ts.completed`. Digests only. Timeout default 2s. No JetStream APIs in this package. + +- [ ] **Step 4: Tests pass** + +Run: `go test ./pkg/tsnats/ -count=1` + +- [ ] **Step 5: Commit** + +```bash +git commit -m "feat: pkg/tsnats digests-only NATS timestamp client" +``` + +--- + +### Task 3: VeraeTime HTTPS client + auth.properties + +**Files:** +- Create: `internal/veraetime/client.go`, `authprops.go`, `*_test.go` + +**Interfaces:** +- Produces: `Stamp(digest)`, `Verify(receiptID)`, `HashReport(digest)`, `Signup`, `Login`, `Session`, `CreateUser` +- Token order: ctor → `VERAE_GLOBAL_TOKEN` → auth.properties +- Stamp body: `data`, `digest`, `hashAlg: SHA256`, `restamp: false` + +- [ ] **Step 1: Fake HTTP round-tripper tests** (existing certificate path + 401 → AuthError) +- [ ] **Step 2: Fail** +- [ ] **Step 3: Implement** +- [ ] **Step 4: Pass** — `go test ./internal/veraetime/ -count=1` +- [ ] **Step 5: Commit** — `feat: VeraeTime client and auth.properties` + +--- + +### Task 4: FileLedger (local/shared) + +**Files:** +- Create: `internal/ledger/file.go`, `file_test.go` + +**Interfaces:** +- `Stamp(digest, scope, cubeID, orgID) ([]Receipt, error)` — idempotent for same hash+org+scope +- `Verify`, `HashReport` against file +- Path mode `0600`; parent dir `0700` + +- [ ] **Step 1–5:** TDD; commit `feat: FileLedger for local and shared scopes` + +--- + +### Task 5: MCP tools + Streamable HTTP server + +**Files:** +- Create: `internal/tools/register.go`, handlers; `internal/server/server.go`; `cmd/verae-mcp-timestamping-server/main.go` +- Depend: go-sdk `mcp.NewStreamableHTTPHandler`, `mcp.AddTool` + +**Routing:** +- `stamp` scope global → veraetime; local|shared → ledger +- `verify` / `hash_report` want_scope=global or CID-like → veraetime; else ledger +- auth tools → veraetime + +- [ ] **Step 1: Integration test** httptest initialize → tools/list → tools/call stamp local +- [ ] **Step 2: Fail** +- [ ] **Step 3: Wire server + main flags** (`-transport`, `-host`, `-port`, env overrides); default shadow-friendly `:8001` in examples only — code default may match Python `8000` with README documenting `8001` for shadow +- [ ] **Step 4:** `go test ./...` and `go build -o bin/verae-mcp-timestamping-server ./cmd/verae-mcp-timestamping-server` +- [ ] **Step 5: Commit** — `feat: MCP Streamable HTTP server with full tool set` + +--- + +### Task 6: internal/natsbridge + stdio transport + +**Files:** +- Create: `internal/natsbridge/bridge.go` wrapping `pkg/tsnats` +- Modify: config + main to enable when `VERAE_NATS_URL` set (ledger backend switch or dual) +- Add stdio serve path via go-sdk stdio transport + +- [ ] **Step 1–5:** TDD with fake tsnats; commit `feat: optional NATS upstream and stdio transport` + +--- + +### Task 7: Docs — cutover, README Usage for tsnats, example + +**Files:** +- Create: `docs/cutover.md`, `examples/tsnats-bulk/main.go` (optional small example) +- Modify: `README.md` — Install/Usage for binary + `pkg/tsnats` import snippet; Standard Readme + +```go +client, err := tsnats.Connect(os.Getenv("NATS_URL")) +receipts, err := client.Stamp(ctx, digest, tsnats.StampOptions{Scope: "global"}) +``` + +- [ ] **Step 1–5:** Write docs; `go test ./...`; commit `docs: cutover checklist and tsnats usage` + +--- + +### Task 8: Shadow unit file + local parity script + +**Files:** +- Create: `deploy/verae-mcp-timestamping-go.service` (PORT=8001) +- Create: `scripts/parity_smoke.sh` — tools/list against `:8000` and `:8001` when both up + +- [ ] Do **not** install on ns1 unless user says publish/deploy. +- [ ] Commit `chore: shadow systemd unit and parity smoke script` + +--- + +## Self-review (plan vs spec) + +| Spec item | Task | +| --- | --- | +| Go MCP binary full tools | Task 5–6 | +| FileLedger | Task 4 | +| VeraeTime + auth.properties | Task 3 | +| pkg/tsnats | Task 2 | +| natsbridge reuses tsnats | Task 6 | +| No JetStream job-complete | Tasks 2, 6, Global Constraints | +| Shadow :8001 + cutover | Tasks 7–8 | +| Digests only | Task 1 (+ enforced everywhere) |