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:
parent
7b5ae5b9aa
commit
3e16839367
1 changed files with 245 additions and 0 deletions
245
docs/superpowers/plans/2026-09-29-go-mcp-timestamping-server.md
Normal file
245
docs/superpowers/plans/2026-09-29-go-mcp-timestamping-server.md
Normal 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) |
|
||||||
Loading…
Reference in a new issue