@helm-protocol/ttt-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,32 @@
1
1
  # @helm-protocol/ttt-mcp
2
2
 
3
- > Reference implementation of [draft-helmprotocol-tttps-00](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) (IETF Experimental)
3
+ > Reference implementation of [draft-helmprotocol-tttps](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) (IETF Experimental)
4
4
 
5
- **MCP Server for OpenTTT Proof of Time tools for AI agents**
5
+ **Proof-of-Time attestation Ed25519-signed timestamps with multi-source corroboration and explicit error bounds. IETF draft-helmprotocol-tttps**
6
+
7
+ ---
8
+
9
+ ## EU AI Act Art. 50 — AI-Generated Content Transparency
10
+
11
+ TTTPS provides cryptographic time-provenance for AI-generated content at the moment of creation. A `pot_generate` call anchors a tamper-evident timestamp to a cryptographic hash of the content record — independently of any embedded metadata.
12
+
13
+ **C2PA complementarity**: C2PA metadata is stripped during recapture, transcoding, and format conversion. TTTPS survives as an external anchor independently verifiable without metadata chain continuity — allowing forensic reconstruction of content provenance even when embedded markers are absent.
14
+
15
+ **GDPR-compatible by design**: PoT records contain no content and no personal identifiers. Each record binds a cryptographic hash (SHA-256 + HMAC-SHA256) to a multi-source time attestation. No plaintext content transits or is stored on Helm servers.
16
+
17
+ IETF specification: [`draft-helmprotocol-tttps`](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) (ISE track). Contact: peter@kenosian.com.
6
18
 
7
19
  ---
8
20
 
9
21
  ## The Problem: Workflow Amnesia
10
22
 
11
- Large Claude Code workflows — 20-agent Dynamic Workflows, multi-day multi-session projects, 100K+ token contexts — all face the same failure mode: **context compression erases action history.**
23
+ Every Claude Code long-horizon workflow hits the same wall: **context compression erases action history.**
12
24
 
13
25
  Agent B has no memory of what Agent A decided. Agent A resumes after compression with no record of its own prior steps. Duplicate work. Lost decisions. State corruption.
14
26
 
15
- **ttt-mcp is the external nervous system that survives context compression.**
27
+ **ttt-mcp is the external causal chain that survives context compression.**
16
28
 
17
- Every workflow step is anchored to a cryptographic timestamp on an **external server** — physically separate from Claude's context window. When compression happens, agents query their exact action history through the MCP tools and resume with full causal context.
29
+ Every workflow step is anchored to a cryptographic timestamp on an **external server** — physically separate from Claude's context window. When compression happens, agents call `pot_query(eventId)` for O(1) exact step recall and resume with full causal context.
18
30
 
19
31
  ```
20
32
  Claude workflow → [context compressed] → agents call pot_query(eventId)
@@ -24,48 +36,109 @@ Claude workflow → [context compressed] → agents call pot_query(eventId)
24
36
 
25
37
  ---
26
38
 
27
- ## Mathematical Guarantee
39
+ ## Mathematical Guarantees
28
40
 
29
41
  | Layer | Mechanism | Guarantee |
30
42
  |-------|-----------|-----------|
31
- | **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶ ≈ 0 — practically 100% exact step recall |
43
+ | **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶ — practically zero |
44
+ | **Lookup** | O(1) exact retrieval | No context consumed by history reconstruction |
32
45
  | **Ordering** | TTTPS causal timestamps | Total order on events — tamper-proof sequence proof |
33
46
  | **Causal chain** | prevEventId DAG | O(depth) traversal — depth ~100 for 1B-token workflows |
34
- | **Fingerprint** | Multi-layer cryptographic pipeline | Formally bounded tamper-evident step identity |
35
47
  | **Non-repudiation** | Ed25519 signature | Cryptographic proof of who acted when |
48
+ | **Resilience** | Erasure-coded cryptographic shards | ≥97% recovery at BER=0.05, 99.88% at BER=0.02 (theoretical) |
49
+ | **Persistence** | Redis AOF + 90-day TTL | Server survives context compression and restarts |
36
50
 
37
51
  ---
38
52
 
39
53
  ## Quick Start
40
54
 
55
+ ### Claude Code
56
+
41
57
  ```bash
42
- # Claude Desktop
58
+ claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.3.0
59
+ ```
60
+
61
+ With an API key (raises the free limit to your plan's monthly quota):
62
+ ```bash
63
+ claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.3.0
64
+ ```
65
+
66
+ ### Claude Desktop
67
+
68
+ Add to `claude_desktop_config.json`:
69
+
43
70
  ```json
44
71
  {
45
72
  "mcpServers": {
46
73
  "ttt": {
47
74
  "command": "npx",
48
- "args": ["-y", "@helm-protocol/ttt-mcp"]
75
+ "args": ["-y", "@helm-protocol/ttt-mcp@0.3.0"],
76
+ "env": { "TTT_API_KEY": "your-key" }
49
77
  }
50
78
  }
51
79
  }
52
80
  ```
53
81
 
54
- Add `TTT_API_KEY` for unlimited calls (free tier: 100 calls/day per IP).
82
+ ### Cursor
83
+
84
+ [![Add to Cursor](https://img.shields.io/badge/Add%20to%20Cursor-1a1a1a?style=flat&logo=cursor&logoColor=white)](https://cursor.com/install-mcp?name=ttt&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBoZWxtLXByb3RvY29sL3R0dC1tY3BAMC4zLjAiXX0=)
85
+
86
+ One-click install, or add the same `mcpServers` block above to `.cursor/mcp.json`.
87
+
88
+ Free tier: 100 calls/day per IP — no signup needed.
89
+
90
+ ---
91
+
92
+ ## 5-Minute Test
93
+
94
+ Once connected, run this sequence in Claude:
95
+
96
+ **Step 1 — Stamp a workflow step:**
97
+
98
+ Just tell Claude naturally:
99
+ > "Stamp this step as my-first-step"
100
+ > "Record what I just did as refactor-auth-step1"
101
+
102
+ Claude calls `pot_generate` automatically. Or call it directly:
103
+ ```
104
+ pot_generate(eventId: "my-first-step")
105
+ ```
106
+
107
+ **Step 2 — Simulate context compression:** start a new Claude session
108
+
109
+ **Step 3 — Recover in the new session:**
110
+
111
+ Tell Claude:
112
+ > "What did I do in my-first-step?"
113
+ > "Recover my last workflow state"
114
+
115
+ Or call directly:
116
+ ```
117
+ pot_query(eventId: "my-first-step")
118
+ ```
119
+ → Returns exact record. Amnesia gone.
120
+
121
+ **Step 4 — Build a causal chain:**
122
+ ```
123
+ pot_generate(eventId: "step-2", prevEventId: "my-first-step")
124
+ pot_graph(eventId: "step-2", depth: 5)
125
+ ```
126
+ → Full backward chain. Cryptographically ordered.
55
127
 
56
128
  ---
57
129
 
58
- ## Tools
130
+ ## 8 Tools
59
131
 
60
- | Tool | Description |
61
- |------|-------------|
62
- | `pot_generate` | Stamp a workflow step with eventId + prevEventId (builds causal chain) |
63
- | `pot_verify` | Verify a Proof of Time using its hash and integrity shards |
64
- | `pot_query` | O(1) exact lookup by eventId call this after context compression |
65
- | `pot_graph` | Traverse full causal DAGbackward + forward chain from any step |
66
- | `pot_stats` | Get turbo/full mode statistics for a time period |
67
- | `pot_health` | Check system health: time sources, uptime, current mode |
68
- | `pot_checkpoint` | Create a compressed rollup checkpoint of workflow history |
132
+ | Tool | Purpose |
133
+ |------|---------|
134
+ | `pot_generate` | Stamp a workflow step with a cryptographic timestamp |
135
+ | `pot_verify` | Verify a PoT signature |
136
+ | `pot_verify_v08` | Verify a draft-08 §3 Payload Digest record |
137
+ | `pot_query` | O(1) exact lookup by eventId core amnesia recovery |
138
+ | `pot_graph` | Traverse causal DAG (backward + forward chain) |
139
+ | `pot_checkpoint` | Roll up events into a compressed summary use every ~100 events or before long tasks |
140
+ | `pot_stats` | Server statistics and mode status |
141
+ | `pot_health` | Health check |
69
142
 
70
143
  ---
71
144
 
@@ -73,7 +146,7 @@ Add `TTT_API_KEY` for unlimited calls (free tier: 100 calls/day per IP).
73
146
 
74
147
  ### pot_generate
75
148
 
76
- Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `eventId` + `prevEventId`. For DeFi: use `txHash` + `chainId` + `poolAddress`. One of `eventId` or `txHash` is required.
149
+ Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `eventId` + `prevEventId`. For DeFi: use `txHash` + `chainId` + `poolAddress`. To bind the attestation to a specific piece of content (draft-08 §3 Payload Digest), also supply `contentDigest`. One of `eventId`, `txHash`, or `contentDigest` is required.
77
150
 
78
151
  | Parameter | Type | Required | Description |
79
152
  |-----------|------|----------|-------------|
@@ -82,6 +155,19 @@ Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `even
82
155
  | txHash | string | Either/or | Transaction hash (DeFi, hex with 0x prefix) |
83
156
  | chainId | number | No | EVM chain ID (DeFi) |
84
157
  | poolAddress | string | No | DEX pool contract address (DeFi) |
158
+ | contentDigest | string | Either/or | SHA-256 digest (lowercase hex, 64 chars) of the content this record attests to. Computed by the caller — the server never sees the content. When the local time synthesis meets draft-08's own requirements (≥3 independent sources, a representable error bound), the response includes a spec-conformant `potRecordV08` binary record (hex); otherwise `potRecordV08Error` explains why not. |
159
+ | ctxId | string | No | draft-08 §3.3 context identifier (Commitment domain separator, max 255 octets). Defaults to a fixed server value; MAY be public. |
160
+
161
+ ### pot_verify_v08
162
+
163
+ Verify a draft-08 §3 record produced by `pot_generate`'s `potRecordV08` field: recomputes the Commitment, checks the Ed25519 signature, and — if `content` is supplied — checks it against the record's Payload Digest.
164
+
165
+ | Parameter | Type | Required | Description |
166
+ |-----------|------|----------|-------------|
167
+ | potRecordV08 | string | Yes | Hex-encoded 184 or 216-octet record |
168
+ | ctxId | string | No | Must match what `pot_generate` used, or verification fails |
169
+ | issuerPubKey | string | No | Hex-encoded 32-byte raw Ed25519 public key. Defaults to this server's own key. |
170
+ | content | string | No | Payload to check against the record's Payload Digest field |
85
171
 
86
172
  ### pot_query
87
173
 
@@ -103,6 +189,19 @@ Traverse the causal chain from any step. Returns backward chain (ancestors) and
103
189
  | eventId | string | Yes | Step to traverse from |
104
190
  | depth | number | No | Max backward depth. Default: 10, max: 100 |
105
191
 
192
+ **Returns:**
193
+ - `backwardChain` — ancestors in chronological order (depth-compressed for large chains)
194
+ - `forwardChain` — steps that follow the given eventId
195
+ - `chainBroken` — `true` if a gap is detected (ancestor was evicted from ring buffer, or the chain root references an unknown entry)
196
+ - `brokenAt` — `"server_restart"` if the gap was caused by a server restart clearing in-memory state; otherwise the eventId at which the break occurred; `null` if chain is intact
197
+ - `reachableDepth` — number of ancestors successfully traversed before the gap (or chain root)
198
+
199
+ **Causal chain gap causes:**
200
+ - **`server_restart`**: the server restarted and the in-memory DAG was cleared. If Redis is available and `REDIS_URL` is set, the DAG is rebuilt from Redis on startup — reducing restart gaps.
201
+ - **Ring-buffer eviction**: the ring buffer holds the most recent 10,000 events in memory. Ancestors beyond that window show as `chainBroken: true` with `brokenAt` set to the oldest reachable eventId.
202
+
203
+ **Recovering from a gap**: call `pot_checkpoint` before long tasks to compress and preserve the chain within the token budget, or use Redis persistence to survive restarts.
204
+
106
205
  ### pot_verify
107
206
 
108
207
  | Parameter | Type | Required | Description |
@@ -138,7 +237,8 @@ Creates a compressed rollup checkpoint of workflow history.
138
237
 
139
238
  **Returns:**
140
239
  - `checkpointId` — unique checkpoint identifier
141
- - `rollupSummary` — compressed event history (depth-adaptive: full/compact/minimal/rollup)
240
+ - `rollup` — compressed event history (depth-adaptive: full/compact/minimal/rollup)
241
+ - `summary` — human-readable one-line summary of the checkpoint
142
242
  - `chainIntact` — whether the causal chain is unbroken
143
243
  - `nextCheckpointHint` — recommended events before next checkpoint
144
244
 
@@ -187,6 +287,19 @@ const chain = await client.callTool({
187
287
  });
188
288
  // chain.backwardChain — all ancestor steps in chronological order
189
289
  // chain.forwardChain — steps that follow this one
290
+ // chain.chainBroken — true if a gap was detected in the ancestor chain
291
+ // chain.brokenAt — "server_restart" if the server restarted and cleared
292
+ // the in-memory DAG; otherwise the eventId of the oldest
293
+ // reachable ancestor before the gap; null if chain intact
294
+ // chain.reachableDepth — how many ancestors were recovered before the gap
295
+
296
+ // Handle a server-restart gap:
297
+ if (chain.chainBroken && chain.brokenAt === "server_restart") {
298
+ // Server cleared in-memory state; ancestors before the gap are gone unless
299
+ // Redis was configured (REDIS_URL) — in that case the DAG was rebuilt on
300
+ // restart and chainBroken will be false.
301
+ // Recover by querying the most recent checkpoint or restarting from a known step.
302
+ }
190
303
  ```
191
304
 
192
305
  **Before a long task or every ~100 events — create a checkpoint:**
@@ -201,7 +314,7 @@ const checkpoint = await client.callTool({
201
314
  }
202
315
  });
203
316
  // checkpoint.checkpointId — store this; resume from it after compression
204
- // checkpoint.rollupSummary — depth-adaptive compressed history (10–200 tokens/event)
317
+ // checkpoint.rollup — depth-adaptive compressed history (10–200 tokens/event)
205
318
  // checkpoint.chainIntact: true — causal chain verified unbroken
206
319
  // checkpoint.nextCheckpointHint: 87 — suggested events before next checkpoint
207
320
 
@@ -230,7 +343,7 @@ const history = await client.callTool({
230
343
 
231
344
  **Problem**: You got front-run. You can't prove it — mempool timestamps are per-node, unsigned, non-authoritative.
232
345
 
233
- **Solution**: Call `pot_generate` before every submission. The PoT receipt is cryptographically signed by three independent time sources (NIST, Google, Cloudflare), anchored on Base Sepolia TTT ERC-1155. If front-running occurs, you have a timestamped, on-chain record predating the attacker's block inclusion.
346
+ **Solution**: Call `pot_generate` before every submission. The PoT receipt is cryptographically signed using three independent time sources (NIST, Google, Cloudflare). The on-chain hash can be anchored via a separate Base Sepolia TTT ERC-1155 contract. If front-running occurs, you have a timestamped record predating the attacker's block inclusion.
234
347
 
235
348
  ```typescript
236
349
  const pot = await client.callTool({
@@ -240,11 +353,15 @@ const pot = await client.callTool({
240
353
  // pot.potHash — your evidence, timestamped by NIST+Google+Cloudflare
241
354
  ```
242
355
 
356
+ > **Note:** The DeFi path (`txHash` + `chainId` + `poolAddress`) requires a server-side build with the integrity-shard pipeline enabled. It is not available in the public `openttt` npm package; calls without it will throw. The Claude Code path (`eventId`) works out of the box.
357
+
243
358
  ---
244
359
 
245
360
  ### 3. DEX Protocol — Sandwich Deterrence
246
361
 
247
- **Solution**: Integrate `TTTHookSimple` (Uniswap V4 hook, Base Sepolia: `0x8C633b05b833a476925F7d9818da6E215760F2c7`). Honest builders get `turbo` mode. Tampered sequences get `full` mode (exponential backoff). Economics, not governance.
362
+ **Solution**: Integrate `TTTHookSimple` (Uniswap V4 hook, Base Sepolia: `0x8C633b05b833a476925F7d9818da6E215760F2c7`). Honest builders get `turbo` mode. Tampered sequences get `full` mode (penalty delay). Economics, not governance.
363
+
364
+ > **Note:** Shard-based verification (`pot_verify` with `grgShards`) requires a server-side build with the integrity-shard pipeline enabled — not available in the public `openttt` npm package.
248
365
 
249
366
  ---
250
367
 
@@ -252,19 +369,21 @@ const pot = await client.callTool({
252
369
 
253
370
  **Problem**: MiFIR Article 22c / RTS 25 requires microsecond-precision UTC-synchronized timestamps. Hardware PTP appliances cost $50K–$500K.
254
371
 
255
- **Solution**: `pot_generate` produces an Ed25519-signed timestamp with uncertainty bound and multi-source attestation. Structurally compatible with RTS 25 audit record requirements. One API call per trade.
372
+ **Solution**: `pot_generate` produces an Ed25519-signed timestamp with an uncertainty bound and multi-source attestation. Structurally compatible with the RTS 25 audit record format. One API call per trade.
256
373
 
257
374
  ```typescript
258
375
  const audit = await client.callTool({
259
376
  name: "pot_generate",
260
377
  arguments: { txHash: tradeHash, chainId: 8453 }
261
378
  });
262
- // audit.timestamp: nanosecond precision
263
- // audit.uncertainty: ±ms bound (RTS 25 required field)
379
+ // audit.timestamp: high-resolution timestamp
380
+ // audit.uncertainty: ± bound (RTS 25 uncertainty field)
264
381
  // audit.confidence: fraction of sources that agreed
265
382
  ```
266
383
 
267
- **Outcome**: MiFIR-grade audit trail. IETF standardized via `draft-helmprotocol-tttps-00`.
384
+ > **Precision note:** The default network time sources (Roughtime / NTP) provide a few-millisecond uncertainty bound. The MiFIR Art. 22c / RTS 25 ±1ms (and tighter) requirement is met only with an added GEO time source (KTSat); this is a roadmap configuration, not the default deployment.
385
+
386
+ **Outcome**: Structurally compatible audit trail. IETF specification: `draft-helmprotocol-tttps`.
268
387
 
269
388
  ---
270
389
 
@@ -276,16 +395,48 @@ const audit = await client.callTool({
276
395
 
277
396
  ---
278
397
 
279
- ## Rate Limits & Pricing
398
+ ## How It Differs — A Different Job, Not "Better"
280
399
 
281
- ```
282
- Free Tier: 100 calls/day per IP — no API key needed
283
- BOT Tier: $199/mo unlimited, SLA
284
- DEX Tier: $499/mo unlimited, priority support
285
- FUND Tier: $2K+/mo enterprise, dedicated infra
286
- ```
400
+ | Tool | Integration | What it recalls | Integrity | Hot-path cost |
401
+ |------|-------------|-----------------|-----------|---------------|
402
+ | Letta (MemGPT) | owns the agent loop | self-editing semantic memory | none | embedding + vector search per memory op |
403
+ | LangGraph / LangMem | LangGraph only | graph state / semantic | none | checkpoint I/O (+ embeddings) |
404
+ | RAG / vector DB | bolt-on | fuzzy similarity | none | embed + vector search per item |
405
+ | **ttt-mcp** | **2-min MCP retrofit** | **exact causal step (by eventId)** | **Ed25519 + TTTPS timestamp** | **sign + hash + write — 0 embedding calls** |
406
+
407
+ **The cost difference is structural, not incidental.**
408
+
409
+ Letta and Mem0 treat agent memory as a semantic search problem — every recall forces an LLM embedding call and a vector search. ttt-mcp bypasses the LLM/embedding layer entirely: state recovery is an O(1) cryptographic hash lookup. Marginal cost is commodity CPU + storage, not API tokens.
410
+
411
+ **Scope**: agents stamp the steps worth checkpointing — not every token, not every query. Volume tracks decisions, not total chat traffic.
412
+
413
+ If you need fuzzy semantic search over past conversations, use Letta or a vector DB. If you need a zero-embedding, deterministic state recovery layer for long-horizon workflows that survives context compaction, use ttt-mcp.
414
+
415
+ ---
287
416
 
288
- Contact: heime.jorgen@proton.me
417
+ ## Pricing
418
+
419
+ | Tier | Price | Calls/month |
420
+ |------|-------|-------------|
421
+ | Free | $0 | 100/day per IP — no signup |
422
+ | Dev | $29/mo | 100K |
423
+ | Pro | $99/mo | 1M |
424
+ | Team | $299/mo | 10M + $0.01/1K overage |
425
+ | Enterprise | $999+/mo | 100M calls/mo · $0.001/1K overage · SLA 99.9% |
426
+ | Platform License | Negotiated ($2M+/yr) | Volume cap negotiated · native integration |
427
+
428
+ **Subscribe:**
429
+
430
+ Dev **$29/mo** · Pro **$99/mo** · Team **$299/mo** — to subscribe, email [peter@kenosian.com](mailto:peter@kenosian.com).
431
+
432
+ Enterprise & Platform License: [peter@kenosian.com](mailto:peter@kenosian.com)
433
+
434
+ Contact: peter@kenosian.com
435
+
436
+ **Quota mechanics — stdio vs HTTP:**
437
+
438
+ - **HTTP mode** (Glama / Smithery container, `PORT` set): the per-IP free tier limit (100 calls/day) is enforced locally in the server process.
439
+ - **stdio mode** (Claude Code `npx`, Claude Desktop): there is no per-IP counter. Tool calls are delegated to `api.kenosian.com` via `X-TTT-API-Key`; quota is enforced server-side against your plan's monthly allowance. Without `TTT_API_KEY` the local fallback runs with no daily cap, but plan features (server-side DAG persistence, multi-session causal chains) are unavailable.
289
440
 
290
441
  ---
291
442
 
@@ -294,12 +445,37 @@ Contact: heime.jorgen@proton.me
294
445
  - Node.js >= 18
295
446
  - Network access for time synthesis (HTTPS to time.nist.gov, time.google.com, time.cloudflare.com)
296
447
 
448
+ **Time source tiers (automatic fallback):**
449
+
450
+ | Tier | Source | Stratum | Notes |
451
+ |------|--------|---------|-------|
452
+ | 1 (preferred) | PTP / hardware clock | 0–1 | Requires local PTP daemon |
453
+ | 2 | Roughtime / NTP (NIST, Google, Cloudflare) | 2–4 | Default for most deployments |
454
+ | 3 (offline fallback) | Local system clock | 16 | RFC 5905 unsynchronized stratum — used when all network sources are unreachable |
455
+
456
+ The server falls through to stratum 16 automatically; no manual configuration needed. The `stratum` field in every `pot_generate` response indicates which tier was used.
457
+
458
+ **Redis persistence (optional):**
459
+
460
+ Redis is not required. The in-memory DAG is authoritative at runtime. If `REDIS_URL` is set, events are written to Redis with a 90-day TTL and the DAG is rebuilt from Redis on server restart — reducing `server_restart` chain gaps. Without Redis, the in-memory DAG is cleared on restart.
461
+
462
+ ---
463
+
464
+ ## Production Tips
465
+
466
+ **Cold Start warm-up** — On first startup, BatchSigner requires one request to initialize. Call `pot_health` or send a single dummy `pot_generate` before your load balancer health check goes live. Without this, the first request may see p99 ~500ms; subsequent requests stabilize to <10ms.
467
+
468
+ ```bash
469
+ # Kubernetes / Docker: add to your startup script
470
+ curl -s http://your-server/pot/health > /dev/null
471
+ ```
472
+
297
473
  ---
298
474
 
299
475
  ## Learn More
300
476
 
301
477
  - [OpenTTT SDK](https://www.npmjs.com/package/openttt) — The underlying SDK
302
- - [IETF Draft: draft-helmprotocol-tttps-00](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) — TTTPS Protocol Specification
478
+ - [IETF Draft: draft-helmprotocol-tttps](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) — TTTPS Protocol Specification
303
479
  - [Helm Protocol](https://github.com/Helm-Protocol) — GitHub
304
480
 
305
481
  ## License
package/dist/auth.js CHANGED
@@ -66,7 +66,7 @@ function writeUsageFile(entry) {
66
66
  }
67
67
  function checkRateLimit(apiKey, clientIp) {
68
68
  if (apiKey && apiKey.trim().length > 0) {
69
- return { allowed: true, remaining: -1, tier: "paid" };
69
+ return { allowed: true, remaining: -1, tier: "paid", serverDelegated: true };
70
70
  }
71
71
  const now = Date.now();
72
72
  if (clientIp === "stdio") {
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ var import_http = require("http");
7
7
  var import_zod = require("zod");
8
8
  var import_tools = require("./tools");
9
9
  var import_auth = require("./auth");
10
+ var import_server = require("./server");
10
11
  async function restoreDAGFromRedis() {
11
12
  try {
12
13
  await Promise.race([
@@ -60,24 +61,68 @@ async function restoreDAGFromRedis() {
60
61
  }
61
62
  return restored;
62
63
  }
64
+ function toolError(err) {
65
+ if (err instanceof import_server.QuotaExceededError) {
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: JSON.stringify(
71
+ { error: "quota_exceeded", tier: err.tier, message: err.message, upgradeUrl: err.upgradeUrl },
72
+ null,
73
+ 2
74
+ )
75
+ }
76
+ ],
77
+ isError: true
78
+ };
79
+ }
80
+ return {
81
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
82
+ isError: true
83
+ };
84
+ }
85
+ function toolSuccess(result) {
86
+ const seal = (0, import_tools.tttsFreshnessSeal)();
87
+ if (seal && result !== null && typeof result === "object") {
88
+ const r = result;
89
+ const notice = r._quotaNotice;
90
+ if (notice) {
91
+ const { _quotaNotice: _, ...rest } = r;
92
+ return {
93
+ content: [
94
+ { type: "text", text: JSON.stringify({ ...rest, _tttps_freshness: seal }, null, 2) },
95
+ { type: "text", text: `\u26A0 Quota notice: ${notice}` }
96
+ ]
97
+ };
98
+ }
99
+ return { content: [{ type: "text", text: JSON.stringify({ ...result, _tttps_freshness: seal }, null, 2) }] };
100
+ }
101
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
102
+ }
63
103
  function buildMcpServer() {
64
- const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.1.0" });
104
+ const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.3.2" });
65
105
  s.tool(
66
106
  "pot_generate",
67
- "Generate a cryptographic Proof of Time timestamp. For Claude Code workflows: use eventId + prevEventId to build a causal chain. For DeFi: use txHash + chainId + poolAddress. Either eventId or txHash is required.",
107
+ "Generate a cryptographic Proof of Time timestamp (draft-helmprotocol-tttps, https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/). For Claude Code workflows: use eventId + prevEventId to build a causal chain. For DeFi: use txHash + chainId + poolAddress. For a spec-conformant draft-08 \xA73 record binding this attestation to a specific piece of content, also supply contentDigest. One of eventId, txHash, or contentDigest is required.",
68
108
  {
69
109
  eventId: import_zod.z.string().optional().describe("Workflow step identifier (Claude Code). E.g. 'refactor_auth_step1'"),
70
110
  prevEventId: import_zod.z.string().optional().describe("Previous step's eventId \u2014 links steps into a causal chain"),
71
111
  txHash: import_zod.z.string().optional().describe("Transaction hash (DeFi, hex with 0x prefix)"),
72
112
  chainId: import_zod.z.number().optional().describe("EVM chain ID (DeFi, e.g. 8453 for Base)"),
73
- poolAddress: import_zod.z.string().optional().describe("DEX pool contract address (DeFi)")
113
+ poolAddress: import_zod.z.string().optional().describe("DEX pool contract address (DeFi)"),
114
+ contentDigest: import_zod.z.string().regex(/^[0-9a-f]{64}$/).optional().describe(
115
+ "SHA-256 digest (lowercase hex, 64 characters) of the content this record attests to. Computed by the caller \u2014 the server never sees the content itself. When supplied, and the local time synthesis meets draft-08's own requirements (>=3 independent sources, a representable error bound), the response includes a spec-conformant potRecordV08 binary record (hex-encoded) in addition to the usual potHash fields; otherwise potRecordV08Error explains why it could not be produced."
116
+ ),
117
+ ctxId: import_zod.z.string().max(255).optional().describe("draft-08 \xA73.3 context identifier (domain separator for the Commitment). Defaults to a fixed server value if omitted; MAY be public.")
74
118
  },
119
+ { title: "Generate Proof of Time", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
75
120
  async (args) => {
76
121
  try {
77
122
  const result = await (0, import_tools.potGenerate)(args);
78
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
123
+ return toolSuccess(result);
79
124
  } catch (err) {
80
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
125
+ return toolError(err);
81
126
  }
82
127
  }
83
128
  );
@@ -90,12 +135,32 @@ function buildMcpServer() {
90
135
  chainId: import_zod.z.number().describe("EVM chain ID (e.g. 84532 for Base Sepolia)"),
91
136
  poolAddress: import_zod.z.string().describe("Uniswap V4 pool address (0x-prefixed)")
92
137
  },
138
+ { title: "Verify Proof of Time", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
93
139
  async (args) => {
94
140
  try {
95
141
  const result = await (0, import_tools.potVerify)(args);
96
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
142
+ return toolSuccess(result);
143
+ } catch (err) {
144
+ return toolError(err);
145
+ }
146
+ }
147
+ );
148
+ s.tool(
149
+ "pot_verify_v08",
150
+ "Verify a draft-helmprotocol-tttps-08 \xA73 Proof-of-Time record (as produced by pot_generate's potRecordV08 field): recomputes the Commitment and checks the Ed25519 signature, and \u2014 if content is supplied \u2014 recomputes SHA-256(content) and checks it against the record's Payload Digest field.",
151
+ {
152
+ potRecordV08: import_zod.z.string().describe("Hex-encoded 184 or 216-octet record from pot_generate's potRecordV08 field"),
153
+ ctxId: import_zod.z.string().max(255).optional().describe("Context identifier the record was generated under. Must match what pot_generate used, or verification fails."),
154
+ issuerPubKey: import_zod.z.string().optional().describe("Hex-encoded 32-byte raw Ed25519 issuer public key. Defaults to this server's own key."),
155
+ content: import_zod.z.string().optional().describe("The payload (utf8) to check against the record's Payload Digest field, if available")
156
+ },
157
+ { title: "Verify draft-08 Proof-of-Time Record", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
158
+ async (args) => {
159
+ try {
160
+ const result = await (0, import_tools.potVerifyV08)(args);
161
+ return toolSuccess(result);
97
162
  } catch (err) {
98
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
163
+ return toolError(err);
99
164
  }
100
165
  }
101
166
  );
@@ -108,12 +173,13 @@ function buildMcpServer() {
108
173
  endTime: import_zod.z.number().optional().describe("End time (unix ms). Default: now"),
109
174
  limit: import_zod.z.number().optional().describe("Max entries to return. Default: 100, max: 1000")
110
175
  },
176
+ { title: "Query Proof of Time Records", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
111
177
  async (args) => {
112
178
  try {
113
179
  const result = await (0, import_tools.potQuery)(args);
114
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
180
+ return toolSuccess(result);
115
181
  } catch (err) {
116
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
182
+ return toolError(err);
117
183
  }
118
184
  }
119
185
  );
@@ -124,12 +190,13 @@ function buildMcpServer() {
124
190
  eventId: import_zod.z.string().describe("The workflow step to start traversal from"),
125
191
  depth: import_zod.z.number().optional().describe("Max backward traversal depth. Default: 10, max: 100")
126
192
  },
193
+ { title: "Traverse PoT Causal Chain", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
127
194
  async (args) => {
128
195
  try {
129
196
  const result = await (0, import_tools.potGraph)(args);
130
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
197
+ return toolSuccess(result);
131
198
  } catch (err) {
132
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
199
+ return toolError(err);
133
200
  }
134
201
  }
135
202
  );
@@ -137,12 +204,13 @@ function buildMcpServer() {
137
204
  "pot_stats",
138
205
  "Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given period.",
139
206
  { period: import_zod.z.enum(["day", "week", "month"]).describe("Time period for statistics") },
207
+ { title: "PoT Statistics", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
140
208
  async (args) => {
141
209
  try {
142
210
  const result = await (0, import_tools.potStats)(args);
143
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
211
+ return toolSuccess(result);
144
212
  } catch (err) {
145
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
213
+ return toolError(err);
146
214
  }
147
215
  }
148
216
  );
@@ -150,12 +218,13 @@ function buildMcpServer() {
150
218
  "pot_health",
151
219
  "Check PoT system health: time source status, subgraph sync, server uptime, and current mode.",
152
220
  {},
221
+ { title: "PoT System Health", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
153
222
  async () => {
154
223
  try {
155
224
  const result = await (0, import_tools.potHealth)();
156
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
225
+ return toolSuccess(result);
157
226
  } catch (err) {
158
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
227
+ return toolError(err);
159
228
  }
160
229
  }
161
230
  );
@@ -169,12 +238,13 @@ function buildMcpServer() {
169
238
  endTime: import_zod.z.number().optional().describe("Unix ms end time (optional, default: now)"),
170
239
  maxTokens: import_zod.z.number().optional().describe("Approximate max tokens for rollup (default: 2000)")
171
240
  },
241
+ { title: "Create PoT Checkpoint", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
172
242
  async (args) => {
173
243
  try {
174
244
  const result = await (0, import_tools.potCheckpoint)(args);
175
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
245
+ return toolSuccess(result);
176
246
  } catch (err) {
177
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
247
+ return toolError(err);
178
248
  }
179
249
  }
180
250
  );
@@ -187,7 +257,7 @@ async function main() {
187
257
  const httpServer = (0, import_http.createServer)(async (req, res) => {
188
258
  if (req.method === "GET" && (req.url === "/health" || req.url === "/ping")) {
189
259
  res.writeHead(200, { "Content-Type": "application/json" });
190
- res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.2.0" }));
260
+ res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.3.2" }));
191
261
  return;
192
262
  }
193
263
  if (req.method === "POST") {
@@ -204,7 +274,8 @@ async function main() {
204
274
  res.end(
205
275
  JSON.stringify({
206
276
  error: "rate_limit_exceeded",
207
- message: "Free tier: 100 calls/day reached. Contact heime.jorgen@proton.me for commercial access.",
277
+ message: import_server.FREE_TIER_UPGRADE_MESSAGE,
278
+ upgradeUrl: import_server.UPGRADE_URL,
208
279
  tier: "free"
209
280
  })
210
281
  );
package/dist/server.js ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var server_exports = {};
20
+ __export(server_exports, {
21
+ FREE_TIER_UPGRADE_MESSAGE: () => FREE_TIER_UPGRADE_MESSAGE,
22
+ QuotaExceededError: () => QuotaExceededError,
23
+ SERVER_BASE_URL: () => SERVER_BASE_URL,
24
+ UPGRADE_MESSAGE: () => UPGRADE_MESSAGE,
25
+ UPGRADE_URL: () => UPGRADE_URL,
26
+ delegateToServer: () => delegateToServer
27
+ });
28
+ module.exports = __toCommonJS(server_exports);
29
+ const SERVER_BASE_URL = (process.env.OPENTTT_SERVER_URL?.trim() || "https://api.kenosian.com").replace(/\/+$/, "");
30
+ const UPGRADE_URL = "https://kenosian.com/products/hydra-mcp.html";
31
+ const UPGRADE_MESSAGE = `Plan quota reached. Upgrade your OpenTTT plan at ${UPGRADE_URL} to continue.`;
32
+ const FREE_TIER_UPGRADE_MESSAGE = `Free tier limit reached (${process.env.FREE_TIER_LIMIT ?? "100"} calls/day). Set TTT_API_KEY with a paid plan, or upgrade at ${UPGRADE_URL}.`;
33
+ class QuotaExceededError extends Error {
34
+ upgradeUrl;
35
+ tier;
36
+ constructor(message, tier) {
37
+ super(message);
38
+ this.name = "QuotaExceededError";
39
+ this.upgradeUrl = UPGRADE_URL;
40
+ this.tier = tier;
41
+ }
42
+ }
43
+ function parseQuotaAdvisory(headers) {
44
+ const remaining = headers.get("x-ratelimit-remaining");
45
+ const limit = headers.get("x-ratelimit-limit");
46
+ const warningHeader = headers.get("x-ratelimit-warning");
47
+ const overage = headers.get("x-ratelimit-overage");
48
+ const tier = headers.get("x-ratelimit-tier");
49
+ const advisory = {};
50
+ let hasContent = false;
51
+ if (tier) {
52
+ advisory.tier = tier;
53
+ hasContent = true;
54
+ }
55
+ if (remaining !== null) {
56
+ advisory.remaining = parseInt(remaining, 10);
57
+ hasContent = true;
58
+ }
59
+ if (limit !== null) {
60
+ advisory.limit = parseInt(limit, 10);
61
+ hasContent = true;
62
+ }
63
+ if (warningHeader) {
64
+ advisory.warning = warningHeader;
65
+ hasContent = true;
66
+ } else if (advisory.remaining !== void 0 && advisory.limit !== void 0 && advisory.limit > 0) {
67
+ const usedRatio = 1 - advisory.remaining / advisory.limit;
68
+ if (usedRatio >= 0.8) {
69
+ advisory.warning = `Approaching plan limit: ${advisory.remaining} of ${advisory.limit} calls remaining this period.`;
70
+ hasContent = true;
71
+ }
72
+ }
73
+ if (overage?.toLowerCase() === "true") {
74
+ advisory.overageActive = true;
75
+ hasContent = true;
76
+ }
77
+ return hasContent ? advisory : void 0;
78
+ }
79
+ function buildUrl(path, query) {
80
+ const url = new URL(SERVER_BASE_URL + path);
81
+ if (query) {
82
+ for (const [k, v] of Object.entries(query)) {
83
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
84
+ }
85
+ }
86
+ return url.toString();
87
+ }
88
+ async function delegateToServer(opts) {
89
+ const { apiKey, method, path, body, query, timeoutMs = 8e3 } = opts;
90
+ const url = buildUrl(path, query);
91
+ const controller = new AbortController();
92
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
93
+ let resp;
94
+ try {
95
+ resp = await fetch(url, {
96
+ method,
97
+ headers: {
98
+ "X-TTT-API-Key": apiKey,
99
+ ...method === "POST" ? { "Content-Type": "application/json" } : {}
100
+ },
101
+ ...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
102
+ signal: controller.signal
103
+ });
104
+ } finally {
105
+ clearTimeout(timer);
106
+ }
107
+ if (resp.status === 429) {
108
+ throw new QuotaExceededError(UPGRADE_MESSAGE, "paid");
109
+ }
110
+ if (!resp.ok) {
111
+ let detail = `HTTP ${resp.status}`;
112
+ try {
113
+ const j = await resp.json();
114
+ if (j?.error) detail = j.error;
115
+ } catch {
116
+ }
117
+ throw new Error(`openttt-server error: ${detail}`);
118
+ }
119
+ const advisory = parseQuotaAdvisory(resp.headers);
120
+ let data;
121
+ try {
122
+ data = await resp.json();
123
+ } catch {
124
+ data = {};
125
+ }
126
+ return advisory ? { data, advisory } : { data };
127
+ }
128
+ // Annotate the CommonJS export names for ESM import in node:
129
+ 0 && (module.exports = {
130
+ FREE_TIER_UPGRADE_MESSAGE,
131
+ QuotaExceededError,
132
+ SERVER_BASE_URL,
133
+ UPGRADE_MESSAGE,
134
+ UPGRADE_URL,
135
+ delegateToServer
136
+ });
package/dist/tools.js CHANGED
@@ -35,17 +35,35 @@ __export(tools_exports, {
35
35
  potQuery: () => potQuery,
36
36
  potStats: () => potStats,
37
37
  potVerify: () => potVerify,
38
+ potVerifyV08: () => potVerifyV08,
38
39
  redis: () => redis,
39
- restoreDagEntry: () => restoreDagEntry
40
+ restoreDagEntry: () => restoreDagEntry,
41
+ tttsFreshnessSeal: () => tttsFreshnessSeal,
42
+ updateLastPot: () => updateLastPot
40
43
  });
41
44
  module.exports = __toCommonJS(tools_exports);
42
45
  var import_openttt = require("openttt");
43
46
  var import_telemetry = require("./telemetry");
47
+ var import_server = require("./server");
48
+ var import_crypto = require("crypto");
49
+ var import_pot_record_v08 = require("./pot_record_v08");
44
50
  var import_ioredis = __toESM(require("ioredis"));
45
51
  const GrgPipeline = (
46
52
  // eslint-disable-next-line @typescript-eslint/no-require-imports
47
53
  require("openttt").GrgPipeline ?? null
48
54
  );
55
+ function applyAdvisory(result, advisory) {
56
+ if (!advisory) return result;
57
+ if (result === null || typeof result !== "object") return result;
58
+ const notices = [];
59
+ if (advisory.warning) notices.push(advisory.warning);
60
+ if (advisory.overageActive) notices.push("Overage billing is active \u2014 usage above your plan limit will be charged.");
61
+ if (notices.length === 0) return result;
62
+ return { ...result, _quotaNotice: notices.join(" | ") };
63
+ }
64
+ function resolvePaidApiKey() {
65
+ return process.env.TTT_API_KEY?.trim() || void 0;
66
+ }
49
67
  const redis = new import_ioredis.default(process.env.REDIS_URL ?? "redis://127.0.0.1:6379", {
50
68
  lazyConnect: true,
51
69
  enableOfflineQueue: false,
@@ -57,6 +75,16 @@ const timeSynth = new import_openttt.TimeSynthesis();
57
75
  const adaptiveSwitch = new import_openttt.AdaptiveSwitch();
58
76
  const potSigner = new import_openttt.PotSigner();
59
77
  const startedAt = Date.now();
78
+ const potSignerPrivateKeyV08 = (0, import_crypto.createPrivateKey)({
79
+ key: Buffer.from(potSigner.getPrivateKeyHex(), "hex"),
80
+ format: "der",
81
+ type: "pkcs8"
82
+ });
83
+ const potSignerPublicKeyRawV08 = Buffer.from(potSigner.getPubKeyHex(), "hex").subarray(-32);
84
+ const potSignerIssuerKeyIdV08 = (0, import_crypto.createHash)("sha256").update(potSignerPublicKeyRawV08).digest().subarray(0, 8);
85
+ const DEFAULT_CTX_ID_V08 = "openttt-mcp/pot_generate";
86
+ const DEFAULT_TIER_V08 = 3;
87
+ const CONTENT_DIGEST_HEX_RE = /^[0-9a-f]{64}$/;
60
88
  const POT_LOG_MAX = 1e4;
61
89
  const potLog = [];
62
90
  const potByEventId = /* @__PURE__ */ new Map();
@@ -101,6 +129,21 @@ function depthThresholdFromTokens(maxTokens, entryCount) {
101
129
  rollup: Math.min(minimalDepth, entryCount)
102
130
  };
103
131
  }
132
+ let _lastPotTimestampNs = null;
133
+ let _lastPotStratum = 16;
134
+ let _lastPotSources = 0;
135
+ function updateLastPot(timestampNs, stratum, sources) {
136
+ _lastPotTimestampNs = timestampNs;
137
+ _lastPotStratum = stratum;
138
+ _lastPotSources = sources;
139
+ }
140
+ function tttsFreshnessSeal() {
141
+ if (_lastPotTimestampNs === null) return null;
142
+ const nowNs = BigInt(Date.now()) * 1000000n;
143
+ const age_ms = Number((nowNs - _lastPotTimestampNs) / 1000000n);
144
+ const ttlMs = _lastPotStratum <= 3 ? 100 : _lastPotStratum <= 8 ? 1e3 : _lastPotStratum <= 15 ? 5e3 : 0;
145
+ return { age_ms, stratum: _lastPotStratum, sources: _lastPotSources, ttlMs };
146
+ }
104
147
  function restoreDagEntry(entry) {
105
148
  if (potByEventId.has(entry.eventId)) return;
106
149
  const e = {
@@ -142,10 +185,29 @@ function restoreDagEntry(entry) {
142
185
  }
143
186
  }
144
187
  async function potGenerate(args) {
145
- if (!args.eventId && !args.txHash) {
146
- throw new Error("Either eventId (Claude Code) or txHash (DeFi) is required");
188
+ if (!args.eventId && !args.txHash && !args.contentDigest) {
189
+ throw new Error("One of eventId (Claude Code), txHash (DeFi), or contentDigest is required");
190
+ }
191
+ if (args.contentDigest !== void 0 && !CONTENT_DIGEST_HEX_RE.test(args.contentDigest)) {
192
+ throw new Error("contentDigest must be a 64-character lowercase hex SHA-256 digest");
147
193
  }
148
194
  (0, import_telemetry.telemetryIncrement)("pot_generate");
195
+ const apiKey = resolvePaidApiKey();
196
+ if (apiKey && args.eventId && !args.contentDigest) {
197
+ const { data, advisory } = await (0, import_server.delegateToServer)({
198
+ apiKey,
199
+ method: "POST",
200
+ path: "/pot/generate",
201
+ body: { eventId: args.eventId, prevEventId: args.prevEventId }
202
+ });
203
+ if (data && typeof data === "object") {
204
+ const d = data;
205
+ if (typeof d.stratum === "number" && typeof d.sources === "number" && typeof d.timestamp === "string") {
206
+ updateLastPot(BigInt(d.timestamp), d.stratum, d.sources);
207
+ }
208
+ }
209
+ return applyAdvisory(data, advisory);
210
+ }
149
211
  let pot;
150
212
  let isOfflineFallback = false;
151
213
  try {
@@ -241,6 +303,34 @@ async function potGenerate(args) {
241
303
  ).catch(() => {
242
304
  });
243
305
  }
306
+ updateLastPot(pot.timestamp, pot.stratum, pot.sources);
307
+ let potRecordV08;
308
+ let potRecordV08CtxId;
309
+ let potRecordV08Error;
310
+ if (args.contentDigest !== void 0) {
311
+ const ctxId = args.ctxId ?? DEFAULT_CTX_ID_V08;
312
+ const errorBoundUs = Math.round(pot.uncertainty * 1e3);
313
+ if (isOfflineFallback || pot.sources < 3) {
314
+ potRecordV08Error = `Src Cnt is ${pot.sources}, but draft-08 \xA73.2 requires at least 3 independent time sources`;
315
+ } else if (errorBoundUs < 0 || errorBoundUs >= import_pot_record_v08.RESERVED_ERROR_BOUND) {
316
+ potRecordV08Error = `Error Bound ${errorBoundUs}us is not representable in the 24-bit field (or hits the reserved value)`;
317
+ } else {
318
+ const fields = {
319
+ version: 1,
320
+ tier: DEFAULT_TIER_V08,
321
+ integrityAlg: import_pot_record_v08.MTI_INTEGRITY_ALG_SHA256,
322
+ srcCnt: pot.sources,
323
+ errorBoundUs,
324
+ timestampNs: pot.timestamp,
325
+ issuerKeyId: potSignerIssuerKeyIdV08,
326
+ nonce: (0, import_crypto.randomBytes)(32),
327
+ payloadDigest: Buffer.from(args.contentDigest, "hex")
328
+ };
329
+ const record = (0, import_pot_record_v08.assemblePotRecordV08)(fields, ctxId, potSignerPrivateKeyV08);
330
+ potRecordV08 = record.toString("hex");
331
+ potRecordV08CtxId = ctxId;
332
+ }
333
+ }
244
334
  return serialize({
245
335
  potHash,
246
336
  eventId: args.eventId ?? null,
@@ -258,7 +348,9 @@ async function potGenerate(args) {
258
348
  issuerPubKey: signature.issuerPubKey,
259
349
  signature: signature.signature,
260
350
  issuedAt: signature.issuedAt.toString()
261
- }
351
+ },
352
+ ...potRecordV08 !== void 0 && { potRecordV08, potRecordV08CtxId, potRecordV08IssuerPubKey: potSignerPublicKeyRawV08.toString("hex") },
353
+ ...potRecordV08Error !== void 0 && { potRecordV08Error }
262
354
  });
263
355
  }
264
356
  async function potVerify(args) {
@@ -282,8 +374,38 @@ async function potVerify(args) {
282
374
  verifiedAt: Date.now()
283
375
  });
284
376
  }
377
+ async function potVerifyV08(args) {
378
+ (0, import_telemetry.telemetryIncrement)("pot_verify_v08");
379
+ const record = Buffer.from(args.potRecordV08, "hex");
380
+ const ctxId = args.ctxId ?? DEFAULT_CTX_ID_V08;
381
+ const issuerPubKey = args.issuerPubKey !== void 0 ? Buffer.from(args.issuerPubKey, "hex") : potSignerPublicKeyRawV08;
382
+ const content = args.content !== void 0 ? Buffer.from(args.content, "utf8") : void 0;
383
+ const result = (0, import_pot_record_v08.verifyPotRecordV08)(record, ctxId, issuerPubKey, content);
384
+ return serialize({
385
+ verdict: result.verdict,
386
+ reason: result.reason ?? null,
387
+ payloadDigestMatchesContent: result.payloadDigestMatchesContent ?? null,
388
+ ctxId,
389
+ verifiedAt: Date.now()
390
+ });
391
+ }
285
392
  async function potQuery(args) {
286
393
  (0, import_telemetry.telemetryIncrement)("pot_query");
394
+ const apiKey = resolvePaidApiKey();
395
+ if (apiKey) {
396
+ const { data, advisory } = await (0, import_server.delegateToServer)({
397
+ apiKey,
398
+ method: "GET",
399
+ path: "/pot/query",
400
+ query: {
401
+ eventId: args.eventId,
402
+ startTimeNs: args.startTime != null ? args.startTime * 1e6 : void 0,
403
+ endTimeNs: args.endTime != null ? args.endTime * 1e6 : void 0,
404
+ limit: args.limit
405
+ }
406
+ });
407
+ return applyAdvisory(data, advisory);
408
+ }
287
409
  if (args.eventId) {
288
410
  const entry = potByEventId.get(args.eventId);
289
411
  return serialize({
@@ -338,6 +460,16 @@ async function potQuery(args) {
338
460
  }
339
461
  async function potGraph(args) {
340
462
  (0, import_telemetry.telemetryIncrement)("pot_graph");
463
+ const apiKey = resolvePaidApiKey();
464
+ if (apiKey) {
465
+ const { data, advisory } = await (0, import_server.delegateToServer)({
466
+ apiKey,
467
+ method: "GET",
468
+ path: "/pot/graph",
469
+ query: { eventId: args.eventId, depth: args.depth }
470
+ });
471
+ return applyAdvisory(data, advisory);
472
+ }
341
473
  const maxDepth = Math.min(args.depth ?? 10, 100);
342
474
  const backwardChain = [];
343
475
  let cursor = potByEventId.get(args.eventId);
@@ -367,6 +499,15 @@ async function potGraph(args) {
367
499
  }
368
500
  async function potStats(args) {
369
501
  (0, import_telemetry.telemetryIncrement)("pot_stats");
502
+ const apiKey = resolvePaidApiKey();
503
+ if (apiKey) {
504
+ const { data, advisory } = await (0, import_server.delegateToServer)({
505
+ apiKey,
506
+ method: "GET",
507
+ path: "/pot/stats"
508
+ });
509
+ return applyAdvisory(data, advisory);
510
+ }
370
511
  const now = Date.now();
371
512
  const periodMs = {
372
513
  day: 864e5,
@@ -447,6 +588,22 @@ async function potHealth() {
447
588
  }
448
589
  async function potCheckpoint(args) {
449
590
  (0, import_telemetry.telemetryIncrement)("pot_checkpoint");
591
+ const apiKey = resolvePaidApiKey();
592
+ if (apiKey) {
593
+ const { data, advisory } = await (0, import_server.delegateToServer)({
594
+ apiKey,
595
+ method: "GET",
596
+ path: "/pot/checkpoint",
597
+ query: {
598
+ fromEventId: args.fromEventId,
599
+ toEventId: args.toEventId,
600
+ startTime: args.startTime,
601
+ endTime: args.endTime,
602
+ maxTokens: args.maxTokens
603
+ }
604
+ });
605
+ return applyAdvisory(data, advisory);
606
+ }
450
607
  const now = Date.now();
451
608
  const startTime = args.startTime ?? now - 36e5;
452
609
  const endTime = args.endTime ?? now;
@@ -470,7 +627,7 @@ async function potCheckpoint(args) {
470
627
  const depthThreshold = args.maxTokens ? depthThresholdFromTokens(args.maxTokens, entries.length) : void 0;
471
628
  const compressed = entries.map((e, i) => compressEntry(e, i + 1, depthThreshold));
472
629
  const chainIntact = !entries.some((e) => e.eventId && evictedEventIds.has(e.eventId));
473
- const nextCheckpointHint = Math.max(10, 100 - eventCount % 100);
630
+ const nextCheckpointHint = Math.max(10, 240 - eventCount % 240);
474
631
  const checkpointId = `ckpt_${now}_${eventCount}`;
475
632
  const firstTs = entries[0]?.timestamp ?? null;
476
633
  const lastTs = entries[entries.length - 1]?.timestamp ?? null;
@@ -493,6 +650,9 @@ async function potCheckpoint(args) {
493
650
  potQuery,
494
651
  potStats,
495
652
  potVerify,
653
+ potVerifyV08,
496
654
  redis,
497
- restoreDagEntry
655
+ restoreDagEntry,
656
+ tttsFreshnessSeal,
657
+ updateLastPot
498
658
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@helm-protocol/ttt-mcp",
3
- "version": "0.3.0",
4
- "description": "MCP Server for OpenTTT Proof of Time tools for AI agents",
3
+ "version": "0.3.2",
4
+ "description": "Proof-of-Time attestation Ed25519-signed timestamps with multi-source corroboration and explicit error bounds. IETF draft-helmprotocol-tttps",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "ttt-mcp": "./dist/index.js"
@@ -11,10 +11,11 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "build": "esbuild index.ts tools.ts telemetry.ts auth.ts --platform=node --target=node18 --format=cjs --outdir=dist",
14
+ "build": "esbuild index.ts tools.ts telemetry.ts auth.ts server.ts --platform=node --target=node18 --format=cjs --outdir=dist",
15
15
  "start": "node dist/index.js",
16
16
  "dev": "npx ts-node index.ts",
17
- "test": "jest --forceExit"
17
+ "test": "jest --forceExit",
18
+ "typecheck": "NODE_OPTIONS='--max-old-space-size=4096' tsc --noEmit"
18
19
  },
19
20
  "jest": {
20
21
  "preset": "ts-jest",
@@ -51,7 +52,7 @@
51
52
  "node": ">=18"
52
53
  },
53
54
  "dependencies": {
54
- "@modelcontextprotocol/sdk": "^1.12.1",
55
+ "@modelcontextprotocol/sdk": "^1.27.1",
55
56
  "ioredis": "^5.11.0",
56
57
  "openttt": "^0.2.13",
57
58
  "zod": "^3.25.0"