Amend design/plan: in-process NATS calling VeraeTime; MCP bearer passthrough when present; VERAE_MCP_REQUIRE_BEARER=0 keeps a server host token when none supplied; =1 requires caller bearer.
10 KiB
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: falseby 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/tsnatssubjects:verae.ts.request,verae.ts.verify,verae.hash.report; subscribeverae.ts.completed.- Embedded NATS in-process (
VERAE_NATS_LISTEN, default127.0.0.1:4223); handlers call VeraeTime/FileLedger. VERAE_MCP_REQUIRE_BEARERdefault0: host-token keeper when no caller bearer;1requires caller bearer.- Shadow default port
8001; Python keeps8000. - 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
// 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
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
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 smallNormalizeSHA256copy inpkg/tsnatsor export via a publicpkg/digest; preferpkg/digestmoved from internal if needed) - Produces:
type Client structConnect(url string, opts ...Option) (*Client, error)Stamp(ctx, sha256 string, opts StampOptions) ([]Receipt, error)→verae.ts.requestVerify(ctx, receiptID string, opts VerifyOptions) (VerifyResult, error)HashReport(ctx, sha256 string, opts ReportOptions) ([]Receipt, error)SubscribeCompleted(ctx, func(CompletedEvent)) (Subscription, error)→ core subscribeverae.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:
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
git commit -m "feat: pkg/tsnats digests-only NATS timestamp client"
Task 3: VeraeTime HTTPS client + auth.properties + host-token keeper
Files:
- Create:
internal/veraetime/client.go,authprops.go,keeper.go,*_test.go - Create:
internal/authctx/resolve.go(request bearer vs host token perRequireBearer)
Interfaces:
-
Produces:
Stamp(digest, bearer),Verify,HashReport,Signup,Login,Session,CreateUser -
Keeper.Token(ctx) (string, error)— refresh via/auth/loginwhen missing/near expiry; orVERAE_GLOBAL_TOKEN -
authctx.Resolve(reqBearer string, requireBearer bool, k *Keeper) (string, error) -
Stamp body:
data,digest,hashAlg: SHA256,restamp: false -
Never log tokens
-
Step 1: Fake HTTP tests (existing cert path; 401 AuthError; keeper refresh; require-bearer rejects empty)
-
Step 2: Fail
-
Step 3: Implement
-
Step 4: Pass —
go test ./internal/veraetime/ ./internal/authctx/ -count=1 -
Step 5: Commit —
feat: VeraeTime client, auth.properties, and host-token keeper
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,HashReportagainst file -
Path mode
0600; parent dir0700 -
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:
-
stampscope global → veraetime; local|shared → ledger -
verify/hash_reportwant_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:8001in examples only — code default may match Python8000with README documenting8001for shadow -
Step 4:
go test ./...andgo 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: Embedded NATS + stdio transport
Files:
-
Create:
internal/embednats/server.go— startnats-serverin-process onVERAE_NATS_LISTEN -
Create:
internal/embednats/handlers.go— queue-subscribeverae.ts.request|verify|hash.report; call ledger/veraetime withauthctx.Resolve; publishverae.ts.completedon core NATS -
Modify: main to start embed when listen addr set; stdio MCP path via go-sdk
-
Step 1: Test embed start + Stamp via
pkg/tsnats.Connectto listen URL -
Step 2–4: Implement; ensure no JetStream for completed
-
Step 5: Commit —
feat: embedded NATS with VeraeTime handlers and stdio MCP
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/tsnatsimport snippet; Standard Readme
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 ./...; commitdocs: 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:8000and:8001when 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) |