docs: cutover checklist and tsnats usage
Add docs/cutover.md (shadow :8001, embedded NATS :4223, REQUIRE_BEARER host-token default, cutover/rollback; no JetStream for job-complete). Expand README Usage with env table and pkg/tsnats snippet; add examples/tsnats-bulk.
This commit is contained in:
parent
e04fe6e2c4
commit
30b5df6757
3 changed files with 182 additions and 3 deletions
68
README.md
68
README.md
|
|
@ -6,7 +6,7 @@ Go MCP timestamping server for Verae. Digests only; sibling to the Python server
|
|||
|
||||
Implements the same tools as https://mcp.verae.ai/mcp (`stamp`, `verify`, `hash_report`, auth tools) using the official MCP Go SDK. Designed for shadow deploy beside Python, then cutover.
|
||||
|
||||
AI assistants use MCP. Bulk app workers can import `pkg/tsnats` for core NATS request/reply on `verae.ts.*` (job-complete stays ephemeral; no JetStream for those events).
|
||||
AI assistants use MCP. Bulk app workers can import `pkg/tsnats` for core NATS request/reply on `verae.ts.*` (job-complete stays ephemeral; no JetStream for those events). Cutover steps: [`docs/cutover.md`](docs/cutover.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
|
@ -22,19 +22,81 @@ cd verae-mcp-timestamping-server-go
|
|||
go build -o bin/verae-mcp-timestamping-server ./cmd/verae-mcp-timestamping-server
|
||||
```
|
||||
|
||||
Requires Go 1.26+. Design: `docs/superpowers/specs/2026-09-29-go-mcp-timestamping-server-design.md`.
|
||||
Requires Go 1.26+. Design: `docs/superpowers/specs/2026-09-29-go-mcp-timestamping-server-design.md`. Shadow systemd unit (not installed by this build): `deploy/verae-mcp-timestamping-go.service`.
|
||||
|
||||
## Usage
|
||||
|
||||
Shadow beside Python (`:8000` stays live):
|
||||
|
||||
```sh
|
||||
VERAE_MCP_TRANSPORT=streamable-http \
|
||||
VERAE_MCP_HOST=127.0.0.1 \
|
||||
VERAE_MCP_PORT=8001 \
|
||||
VERAE_NATS_LISTEN=127.0.0.1:4223 \
|
||||
VERAE_MCP_REQUIRE_BEARER=0 \
|
||||
./bin/verae-mcp-timestamping-server
|
||||
```
|
||||
|
||||
Point clients at `http://127.0.0.1:8001/mcp`. Global stamps use `https://api.veraetime.net` with auth from `~/.verae/auth.properties` (or `VERAE_AUTH_PROPERTIES`). Embedded NATS (default `127.0.0.1:4223`; set `VERAE_NATS_LISTEN=` empty to disable) serves `verae.ts.*` with the same ledger/VeraeTime routing; job-complete is core `verae.ts.completed` only. For stdio MCP: `-transport stdio` or `VERAE_MCP_TRANSPORT=stdio`.
|
||||
Point MCP clients at `http://127.0.0.1:8001/mcp`. For stdio: `-transport stdio` or `VERAE_MCP_TRANSPORT=stdio`.
|
||||
|
||||
| Variable | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `VERAE_MCP_TRANSPORT` | `streamable-http` | or `stdio` |
|
||||
| `VERAE_MCP_HOST` | `127.0.0.1` | HTTP bind |
|
||||
| `VERAE_MCP_PORT` | `8000` | shadow examples use `8001` |
|
||||
| `VERAE_NATS_LISTEN` | `127.0.0.1:4223` | embedded NATS; set empty to disable |
|
||||
| `VERAE_MCP_REQUIRE_BEARER` | `0` | `0` = host-token keeper when no caller bearer; `1` = require caller bearer |
|
||||
| `VERAE_GLOBAL_URL` | `https://api.veraetime.net` | |
|
||||
| `VERAE_AUTH_PROPERTIES` | `$HOME/.verae/auth.properties` | |
|
||||
| `VERAE_GLOBAL_TOKEN` | empty | optional static host bearer |
|
||||
| `VERAE_LEDGER_PATH` | `$HOME/.verae/mcp-timestamp-local.json` | |
|
||||
| `VERAE_MCP_CORS` | off | set `1` for browser CORS |
|
||||
|
||||
Global stamps use VeraeTime with auth from `auth.properties` (or caller Bearer). Embedded NATS serves `verae.ts.*`; job-complete is core `verae.ts.completed` only.
|
||||
|
||||
Parity smoke when Python `:8000` and Go `:8001` are both up:
|
||||
|
||||
```sh
|
||||
./scripts/parity_smoke.sh
|
||||
```
|
||||
|
||||
### pkg/tsnats
|
||||
|
||||
Bulk workers talk to the embedded (or any) NATS URL. Digests only; `SubscribeCompleted` is core NATS, not JetStream.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/pkg/tsnats"
|
||||
)
|
||||
|
||||
func main() {
|
||||
url := os.Getenv("NATS_URL")
|
||||
if url == "" {
|
||||
url = "nats://127.0.0.1:4223"
|
||||
}
|
||||
client, err := tsnats.Connect(url)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
digest := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
receipts, err := client.Stamp(ctx, digest, tsnats.StampOptions{Scope: "global"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("receipts: %+v", receipts)
|
||||
}
|
||||
```
|
||||
|
||||
Runnable sketch: `examples/tsnats-bulk/main.go`.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
67
docs/cutover.md
Normal file
67
docs/cutover.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Shadow deploy and cutover (Go MCP)
|
||||
|
||||
Python stays on `127.0.0.1:8000`. Go shadows on `127.0.0.1:8001` with embedded NATS on `127.0.0.1:4223`. Do **not** install or enable the Go unit on ns1 until an explicit publish/deploy order.
|
||||
|
||||
## Shadow layout
|
||||
|
||||
| Role | Bind | Notes |
|
||||
| --- | --- | --- |
|
||||
| Python MCP (live) | `127.0.0.1:8000` | Caddy → public `/mcp` |
|
||||
| Go MCP (shadow) | `127.0.0.1:8001` | Internal smoke only until cutover |
|
||||
| Go embedded NATS | `127.0.0.1:4223` | `pkg/tsnats` / bulk workers |
|
||||
|
||||
Install tree (when deploying): `/opt/verae/verae-mcp-timestamping-server-go/` (does not overwrite the Python venv). Unit file in-repo: `deploy/verae-mcp-timestamping-go.service`.
|
||||
|
||||
## Bearer / host-token (shadow default)
|
||||
|
||||
| `VERAE_MCP_REQUIRE_BEARER` | Behavior |
|
||||
| --- | --- |
|
||||
| `0` or unset (**default**) | Caller `Authorization: Bearer` wins when present. Otherwise the process keeps a host token from `~/.verae/auth.properties` (or `VERAE_AUTH_PROPERTIES` / `VERAE_GLOBAL_TOKEN`) and refreshes before expiry. |
|
||||
| `1` | Caller bearer required for global VeraeTime paths; no host-token fallback. |
|
||||
|
||||
Shadow unit ships `VERAE_MCP_REQUIRE_BEARER=0` so operators can smoke without every client supplying a JWT.
|
||||
|
||||
## Embedded NATS / job-complete
|
||||
|
||||
- Listen: `VERAE_NATS_LISTEN=127.0.0.1:4223` (set empty to disable embed).
|
||||
- Request/reply: `verae.ts.request`, `verae.ts.verify`, `verae.hash.report`.
|
||||
- Job-complete / jobId notify: **core NATS only** on `verae.ts.completed` (and receipt notify if used). **No JetStream** (or other persistent stream) for job-complete. Publishers fire-and-forget; listeners subscribe.
|
||||
|
||||
## Env cheat sheet (shadow)
|
||||
|
||||
```sh
|
||||
VERAE_MCP_TRANSPORT=streamable-http
|
||||
VERAE_MCP_HOST=127.0.0.1
|
||||
VERAE_MCP_PORT=8001
|
||||
VERAE_NATS_LISTEN=127.0.0.1:4223
|
||||
VERAE_MCP_REQUIRE_BEARER=0
|
||||
# optional: VERAE_AUTH_PROPERTIES, VERAE_LEDGER_PATH, VERAE_GLOBAL_URL, VERAE_GLOBAL_TOKEN, VERAE_MCP_CORS
|
||||
```
|
||||
|
||||
Service user on ns1 is `verae-mcp` with `HOME=/var/lib/verae-mcp` (share or isolate `auth.properties` vs Python as chosen for the soak).
|
||||
|
||||
## Local parity before soak
|
||||
|
||||
With Python on `:8000` and Go on `:8001`:
|
||||
|
||||
```sh
|
||||
./scripts/parity_smoke.sh
|
||||
```
|
||||
|
||||
Compares `tools/list` name sets. Optionally stamp/verify/hash_report the same digests against both ports during soak.
|
||||
|
||||
## Cutover checklist
|
||||
|
||||
1. Shadow green for the agreed soak (tools/list parity, global stamp/verify/hash_report, local ledger, NATS stamp + `verae.ts.completed` observe).
|
||||
2. Stop Python unit **or** switch Caddy upstream away from `:8000` Python.
|
||||
3. Point Go at public port: set `VERAE_MCP_PORT=8000` (or leave Go on `8001` and point Caddy at Go).
|
||||
4. Confirm `https://mcp.verae.ai/mcp` `tools/list` and a known digest `hash_report`.
|
||||
5. Keep the Python tree on disk for one release cycle.
|
||||
6. Do not enable JetStream for job-complete as part of cutover.
|
||||
|
||||
## Rollback checklist
|
||||
|
||||
1. Restore Python `ExecStart` / Caddy upstream to `127.0.0.1:8000` Python.
|
||||
2. Stop or disable `verae-mcp-timestamping-go.service` (or move Go back to `:8001` only).
|
||||
3. Restart Python; confirm public `tools/list` and a known `hash_report`.
|
||||
4. Leave Go binary under `/opt/verae/verae-mcp-timestamping-server-go/` for a later retry; do not delete until soak of the rollback is confirmed.
|
||||
50
examples/tsnats-bulk/main.go
Normal file
50
examples/tsnats-bulk/main.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Command tsnats-bulk is a minimal digests-only stamp client against embedded NATS.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/pkg/tsnats"
|
||||
)
|
||||
|
||||
func main() {
|
||||
natsURL := flag.String("nats", envOr("NATS_URL", "nats://127.0.0.1:4223"), "NATS URL (embedded default :4223)")
|
||||
digest := flag.String("digest", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "SHA-256 hex")
|
||||
scope := flag.String("scope", "local", "stamp scope: local|shared|global")
|
||||
flag.Parse()
|
||||
|
||||
client, err := tsnats.Connect(*natsURL, tsnats.WithTimeout(5*time.Second))
|
||||
if err != nil {
|
||||
log.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
sub, err := client.SubscribeCompleted(ctx, func(ev tsnats.CompletedEvent) {
|
||||
fmt.Printf("completed jobId=%s receipt_id=%s hash=%s\n", ev.JobID, ev.ReceiptID, ev.Hash)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("subscribe completed: %v", err)
|
||||
}
|
||||
defer func() { _ = sub.Unsubscribe() }()
|
||||
|
||||
receipts, err := client.Stamp(ctx, *digest, tsnats.StampOptions{Scope: *scope})
|
||||
if err != nil {
|
||||
log.Fatalf("stamp: %v", err)
|
||||
}
|
||||
for _, r := range receipts {
|
||||
fmt.Printf("receipt_id=%s hash=%s scope=%s\n", r.ReceiptID, r.Hash, r.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Loading…
Reference in a new issue