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
Date: 2026-09-29
Status: Draft for review
Sibling of: verae-mcp-timestamping-server (Python, live on ns1)
Contract: Same digests-only MCP tools as https://mcp.verae.ai/mcp and Application Client OpenAPI
Problem
The live Verae MCP timestamping service is a Python process (verae-mcp-timestamping-server in a venv, Streamable HTTP on 127.0.0.1:8000). Operators want a Go executable with full tool and ledger parity, suitable for shadow deploy and eventual cutover, without changing the public MCP contract.
Goals
- Single static Go binary implementing the MCP timestamping server.
- Full tool parity:
stamp,verify,hash_report,signup,login,session,create_user. - Local/shared
FileLedger(JSON file, mode 0600) and global VeraeTime HTTPS client. - Embedded NATS server in-process (nats-server library); handlers call VeraeTime directly.
- Bearer handling (configurable):
VERAE_MCP_REQUIRE_BEARER=0(default): if the MCP/NATS request suppliesAuthorization: Bearer, use it (passthrough). If none is supplied, the server obtains and keeps a current host token (login viaauth.propertiesusername/password orVERAE_GLOBAL_TOKEN, refresh before expiry) and uses that for VeraeTime.VERAE_MCP_REQUIRE_BEARER=1: missing/invalid bearer is rejected; no host-token fallback for global VeraeTime calls.
- Streamable HTTP + stdio transports; CORS / allowed hosts/origins compatible with ns1.
- Shadow on ns1 beside Python; documented cutover checklist.
- Importable
pkg/tsnatsclient for bulk workers connecting to the embedded (or external) NATS listen address. - Job-complete remains core NATS publish/subscribe; no JetStream for job-complete.
Non-goals (this project)
- Changing live MCP tool schemas or Application Client OpenAPI semantics.
- Replacing Python on day one without a shadow period.
- JetStream (or other persistent streams) for job-complete / jobId notification.
- Accepting file bytes / content on stamp tools.
Current state (Python)
- Package:
verae-mcp-timestamping-server(Python ≥3.10,mcp[cli]). - Deploy:
/opt/verae/verae-mcp-timestamping-server, userverae-mcp, systemdverae-mcp-timestamping.service. - Public: Caddy →
127.0.0.1:8000formcp.verae.ai,mcp.veraetime.net,mcp.georgelambert.org. - Global stamps:
POST https://api.veraetime.net/api/timestampwithrestamp: false.
Architecture
AI assistants
│ MCP Streamable HTTP / stdio
│ Authorization: Bearer <user JWT>
▼
verae-mcp-timestamping-server-go (single binary)
├── MCP tools (AI) ──► FileLedger (local|shared) / VeraeTime (global)
│ bearer from MCP request context (passthrough)
├── Embedded NATS server (nats-server, in-process)
│ subscribe request/reply:
│ verae.ts.request → VeraeTime or FileLedger
│ verae.ts.verify → VeraeTime or FileLedger
│ verae.hash.report → VeraeTime or FileLedger
│ publish core notify: verae.ts.completed (no JetStream)
│ token: from NATS msg header/field OR from MCP-originated publish context
└── Listen: MCP :8001 (shadow) + NATS client port (e.g. 4223) for pkg/tsnats
Bulk workers ──pkg/tsnats──► embedded NATS port
Split: MCP tools stay for AI. Embedded NATS is the high-concurrency messaging plane; both paths call the same VeraeTime client with per-request bearer.
SDK: official github.com/modelcontextprotocol/go-sdk/mcp.
Embedded NATS: github.com/nats-io/nats-server/v2 + nats.go.
Client lib: pkg/tsnats connects to the embedded listen URL (or an external NATS URL for tests).
Repository layout
Path: /Users/marchon/research/modelcontextprotocol/verae-mcp-timestamping-server-go
Remote (planned): ssh://git@git.georgelambert.org:2223/marchon/verae-mcp-timestamping-server-go.git
cmd/verae-mcp-timestamping-server/main.go
pkg/digest/ # NormalizeSHA256, AssertDigestOnly (done)
pkg/tsnats/ # importable client → embedded or external NATS
internal/config/
internal/ledger/ # FileLedger
internal/veraetime/ # GlobalClient; per-request Bearer override
internal/embednats/ # start embedded nats-server; register subject handlers
internal/authctx/ # extract MCP bearer → context; attach to VeraeTime + NATS
internal/tools/ # MCP tool handlers
internal/server/ # MCP HTTP/stdio wiring
docs/superpowers/specs/
docs/cutover.md
README.md
Binary name: verae-mcp-timestamping-server
Install dir on ns1 (shadow): /opt/verae/verae-mcp-timestamping-server-go/ so it does not overwrite the Python venv path.
Tool contract
Match Python docs/api.md / live tools/list:
| Tool | Required args | Notes |
|---|---|---|
stamp |
sha256 |
scope local|shared|global; optional cube_id, org_id |
verify |
receipt_id |
optional want_scope; global or CID-like id → VeraeTime |
hash_report |
sha256 |
optional org_id, want_scope |
signup |
username, password |
VeraeTime /auth/signup |
login |
username, password |
stores JWT in auth.properties; does not return JWT |
session |
— | /auth/validate |
create_user |
username, password, role |
admin bearer |
Digests only: reject content/bytes/file/data/path/body/payload/blob/archive.
Normalize SHA-256 (optional sha256: prefix; 64 hex).
Global stamp: restamp: false; poll /api/status/{jobId} when needed.
Auth / bearer policy
| Mode | Config | Behavior |
|---|---|---|
| Host-token fallback (default) | VERAE_MCP_REQUIRE_BEARER=0 or unset |
Resolve bearer: request Bearer → else in-memory host token (mint/refresh via VeraeTime /auth/login using username/password from auth.properties, or use VERAE_GLOBAL_TOKEN / stored access_token). Never log the token. |
| Require caller bearer | VERAE_MCP_REQUIRE_BEARER=1 |
Request must include Bearer; validate with VeraeTime /auth/validate (or reject 401 from upstream). No host fallback for stamp/verify/hash_report global paths. |
login tool still writes auth.properties for operators. Host-token keeper loads from that file and refreshes when expires_at is near (skew ~60s). MCP session may cache the resolved bearer for the session lifetime when require-bearer is off.
Embedded NATS handlers use the same resolver: message/header token if present, else host token when require-bearer is off.
pkg/tsnats (bulk app client)
Import path (planned): git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/pkg/tsnats
| API | Behavior |
|---|---|
Connect(url string, opts ...Option) (*Client, error) |
Core NATS connection |
Stamp(ctx, sha256, opts) |
Request/reply on verae.ts.request; digests only |
Verify(ctx, receiptID, opts) |
verae.ts.verify |
HashReport(ctx, sha256, opts) |
verae.hash.report |
SubscribeCompleted(ctx, handler) |
Core subscribe to verae.ts.completed (and documented receipt alias if any); not JetStream |
Defaults: request timeout ~2s (configurable); concurrent Stamp calls are safe; empty/invalid digests fail client-side before publish.
The MCP server’s optional NATS upstream (internal/natsbridge) imports this package so bus encoding stays one implementation.
Bulk guidance: prefer many in-flight Stamp calls (or fire request + SubscribeCompleted) over serial MCP tools/call. For central global certificates when not on the bus, Application Client / VeraeTime HTTPS remains valid.
Config (env)
| Variable | Default |
|---|---|
VERAE_MCP_TRANSPORT |
streamable-http (or stdio) |
VERAE_MCP_HOST |
127.0.0.1 |
VERAE_MCP_PORT |
8000 (shadow uses 8001) |
VERAE_GLOBAL_URL |
https://api.veraetime.net |
VERAE_AUTH_PROPERTIES |
$HOME/.verae/auth.properties |
VERAE_GLOBAL_TOKEN |
empty (optional static host bearer) |
VERAE_MCP_REQUIRE_BEARER |
0 (off = host-token fallback; 1 = require caller bearer) |
VERAE_LEDGER_PATH |
$HOME/.verae/mcp-timestamp-local.json |
VERAE_NATS_LISTEN |
127.0.0.1:4223 (embedded NATS client port; empty disables embed) |
VERAE_MCP_CORS |
unset / 1 |
VERAE_MCP_ALLOWED_HOSTS |
same list as Python verae-ai.conf |
VERAE_MCP_ALLOWED_ORIGINS |
same list as Python |
HOME for the service user remains /var/lib/verae-mcp (shared auth file with Python during shadow if desired, or separate path for isolation).
Shadow deploy
- Build linux amd64 (and optionally arm64) release binary.
- Install under
/opt/verae/verae-mcp-timestamping-server-go/. - Systemd unit
verae-mcp-timestamping-go.service:VERAE_MCP_PORT=8001, same userverae-mcp, same or dedicated auth properties. - Optional Caddy path or internal-only listen for smoke (do not steal public
/mcpuntil cutover). - Compare
tools/listand stamp/verify/hash_report against Python on:8000.
Cutover checklist (docs/cutover.md)
- Shadow green for agreed soak (tools + global stamp/verify/hash_report + local ledger).
- Stop Python unit (or switch Caddy upstream).
- Point Go unit at
VERAE_MCP_PORT=8000(or Caddy to Go’s port). - Confirm
https://mcp.verae.ai/mcptools/list and a known digesthash_report. - Keep Python tree on disk for rollback for one release cycle.
- Rollback: restore Python
ExecStart/ Caddy upstream; restart.
Testing
- Unit: digest normalize/guard; FileLedger idempotency; VeraeTime request shaping (
restamp: false) with fake HTTP. - Integration: Streamable HTTP initialize → tools/call against an httptest test server.
- Parity script: same digests against Python
:8000and Go:8001. - Optional live smoke behind flag (requires auth.properties).
Success criteria
go buildproduces a single runnable binary.- Full tool list matches Python.
- Local/shared persist to ledger file; global uses VeraeTime with caller bearer or host-token fallback per
VERAE_MCP_REQUIRE_BEARER. - Embedded NATS (when enabled) handles
verae.ts.*and does not use JetStream for job-complete. pkg/tsnatsbuilds as an importable library with unit tests against embedded or test NATS.- Host-token keeper refreshes before expiry when require-bearer is off.
- Shadow unit runs on ns1 without disturbing Python
:8000. - Cutover doc is complete enough to swap without inventing steps on the day.
Open follow-ups
- Whether shadow shares
/var/lib/verae-mcp/auth.propertieswith Python or uses a separate file. - Whether public hostname gets a temporary Go-only path (e.g.
/mcp-go) during soak.