docs: implementation plan for Go MCP server and pkg/tsnats

Eight TDD tasks: digest module, tsnats client, VeraeTime, FileLedger,
MCP HTTP server, NATS bridge/stdio, cutover docs, shadow unit. Spec
already includes AI MCP vs bulk NATS split.
This commit is contained in:
George Lambert 2026-09-29 11:41:55 -04:00
commit 3e16839367

View file

@ -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) |