@helm-protocol/ttt-mcp 0.3.0 → 0.3.1

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,6 +1,6 @@
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
5
  **MCP Server for OpenTTT — Proof of Time tools for AI agents**
6
6
 
@@ -8,13 +8,13 @@
8
8
 
9
9
  ## The Problem: Workflow Amnesia
10
10
 
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.**
11
+ Every Claude Code long-horizon workflow hits the same wall: **context compression erases action history.**
12
12
 
13
13
  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
14
 
15
- **ttt-mcp is the external nervous system that survives context compression.**
15
+ **ttt-mcp is the external causal chain that survives context compression.**
16
16
 
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.
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 call `pot_query(eventId)` for O(1) exact step recall and resume with full causal context.
18
18
 
19
19
  ```
20
20
  Claude workflow → [context compressed] → agents call pot_query(eventId)
@@ -24,48 +24,108 @@ Claude workflow → [context compressed] → agents call pot_query(eventId)
24
24
 
25
25
  ---
26
26
 
27
- ## Mathematical Guarantee
27
+ ## Mathematical Guarantees
28
28
 
29
29
  | Layer | Mechanism | Guarantee |
30
30
  |-------|-----------|-----------|
31
- | **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶ ≈ 0 — practically 100% exact step recall |
31
+ | **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶ — practically zero |
32
+ | **Lookup** | O(1) exact retrieval | No context consumed by history reconstruction |
32
33
  | **Ordering** | TTTPS causal timestamps | Total order on events — tamper-proof sequence proof |
33
34
  | **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
35
  | **Non-repudiation** | Ed25519 signature | Cryptographic proof of who acted when |
36
+ | **Resilience** | Erasure-coded cryptographic shards | ≥97% recovery at BER=0.05, 99.88% at BER=0.02 (theoretical) |
37
+ | **Persistence** | Redis AOF + 90-day TTL | Server survives context compression and restarts |
36
38
 
37
39
  ---
38
40
 
39
41
  ## Quick Start
40
42
 
43
+ ### Claude Code
44
+
45
+ ```bash
46
+ claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.3.0
47
+ ```
48
+
49
+ With an API key (raises the free limit to your plan's monthly quota):
41
50
  ```bash
42
- # Claude Desktop
51
+ claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.3.0
52
+ ```
53
+
54
+ ### Claude Desktop
55
+
56
+ Add to `claude_desktop_config.json`:
57
+
43
58
  ```json
44
59
  {
45
60
  "mcpServers": {
46
61
  "ttt": {
47
62
  "command": "npx",
48
- "args": ["-y", "@helm-protocol/ttt-mcp"]
63
+ "args": ["-y", "@helm-protocol/ttt-mcp@0.3.0"],
64
+ "env": { "TTT_API_KEY": "your-key" }
49
65
  }
50
66
  }
51
67
  }
52
68
  ```
53
69
 
54
- Add `TTT_API_KEY` for unlimited calls (free tier: 100 calls/day per IP).
70
+ ### Cursor
71
+
72
+ [![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=)
73
+
74
+ One-click install, or add the same `mcpServers` block above to `.cursor/mcp.json`.
75
+
76
+ Free tier: 100 calls/day per IP — no signup needed.
77
+
78
+ ---
79
+
80
+ ## 5-Minute Test
81
+
82
+ Once connected, run this sequence in Claude:
83
+
84
+ **Step 1 — Stamp a workflow step:**
85
+
86
+ Just tell Claude naturally:
87
+ > "Stamp this step as my-first-step"
88
+ > "Record what I just did as refactor-auth-step1"
89
+
90
+ Claude calls `pot_generate` automatically. Or call it directly:
91
+ ```
92
+ pot_generate(eventId: "my-first-step")
93
+ ```
94
+
95
+ **Step 2 — Simulate context compression:** start a new Claude session
96
+
97
+ **Step 3 — Recover in the new session:**
98
+
99
+ Tell Claude:
100
+ > "What did I do in my-first-step?"
101
+ > "Recover my last workflow state"
102
+
103
+ Or call directly:
104
+ ```
105
+ pot_query(eventId: "my-first-step")
106
+ ```
107
+ → Returns exact record. Amnesia gone.
108
+
109
+ **Step 4 — Build a causal chain:**
110
+ ```
111
+ pot_generate(eventId: "step-2", prevEventId: "my-first-step")
112
+ pot_graph(eventId: "step-2", depth: 5)
113
+ ```
114
+ → Full backward chain. Cryptographically ordered.
55
115
 
56
116
  ---
57
117
 
58
- ## Tools
118
+ ## 7 Tools
59
119
 
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 DAG backward + 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 |
120
+ | Tool | Purpose |
121
+ |------|---------|
122
+ | `pot_generate` | Stamp a workflow step with a cryptographic timestamp |
123
+ | `pot_verify` | Verify a PoT signature |
124
+ | `pot_query` | O(1) exact lookup by eventId — core amnesia recovery |
125
+ | `pot_graph` | Traverse causal DAG (backward + forward chain) |
126
+ | `pot_checkpoint` | Roll up events into a compressed summary — use every ~100 events or before long tasks |
127
+ | `pot_stats` | Server statistics and mode status |
128
+ | `pot_health` | Health check |
69
129
 
70
130
  ---
71
131
 
@@ -103,6 +163,19 @@ Traverse the causal chain from any step. Returns backward chain (ancestors) and
103
163
  | eventId | string | Yes | Step to traverse from |
104
164
  | depth | number | No | Max backward depth. Default: 10, max: 100 |
105
165
 
166
+ **Returns:**
167
+ - `backwardChain` — ancestors in chronological order (depth-compressed for large chains)
168
+ - `forwardChain` — steps that follow the given eventId
169
+ - `chainBroken` — `true` if a gap is detected (ancestor was evicted from ring buffer, or the chain root references an unknown entry)
170
+ - `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
171
+ - `reachableDepth` — number of ancestors successfully traversed before the gap (or chain root)
172
+
173
+ **Causal chain gap causes:**
174
+ - **`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.
175
+ - **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.
176
+
177
+ **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.
178
+
106
179
  ### pot_verify
107
180
 
108
181
  | Parameter | Type | Required | Description |
@@ -138,7 +211,8 @@ Creates a compressed rollup checkpoint of workflow history.
138
211
 
139
212
  **Returns:**
140
213
  - `checkpointId` — unique checkpoint identifier
141
- - `rollupSummary` — compressed event history (depth-adaptive: full/compact/minimal/rollup)
214
+ - `rollup` — compressed event history (depth-adaptive: full/compact/minimal/rollup)
215
+ - `summary` — human-readable one-line summary of the checkpoint
142
216
  - `chainIntact` — whether the causal chain is unbroken
143
217
  - `nextCheckpointHint` — recommended events before next checkpoint
144
218
 
@@ -187,6 +261,19 @@ const chain = await client.callTool({
187
261
  });
188
262
  // chain.backwardChain — all ancestor steps in chronological order
189
263
  // chain.forwardChain — steps that follow this one
264
+ // chain.chainBroken — true if a gap was detected in the ancestor chain
265
+ // chain.brokenAt — "server_restart" if the server restarted and cleared
266
+ // the in-memory DAG; otherwise the eventId of the oldest
267
+ // reachable ancestor before the gap; null if chain intact
268
+ // chain.reachableDepth — how many ancestors were recovered before the gap
269
+
270
+ // Handle a server-restart gap:
271
+ if (chain.chainBroken && chain.brokenAt === "server_restart") {
272
+ // Server cleared in-memory state; ancestors before the gap are gone unless
273
+ // Redis was configured (REDIS_URL) — in that case the DAG was rebuilt on
274
+ // restart and chainBroken will be false.
275
+ // Recover by querying the most recent checkpoint or restarting from a known step.
276
+ }
190
277
  ```
191
278
 
192
279
  **Before a long task or every ~100 events — create a checkpoint:**
@@ -201,7 +288,7 @@ const checkpoint = await client.callTool({
201
288
  }
202
289
  });
203
290
  // checkpoint.checkpointId — store this; resume from it after compression
204
- // checkpoint.rollupSummary — depth-adaptive compressed history (10–200 tokens/event)
291
+ // checkpoint.rollup — depth-adaptive compressed history (10–200 tokens/event)
205
292
  // checkpoint.chainIntact: true — causal chain verified unbroken
206
293
  // checkpoint.nextCheckpointHint: 87 — suggested events before next checkpoint
207
294
 
@@ -230,7 +317,7 @@ const history = await client.callTool({
230
317
 
231
318
  **Problem**: You got front-run. You can't prove it — mempool timestamps are per-node, unsigned, non-authoritative.
232
319
 
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.
320
+ **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
321
 
235
322
  ```typescript
236
323
  const pot = await client.callTool({
@@ -240,11 +327,15 @@ const pot = await client.callTool({
240
327
  // pot.potHash — your evidence, timestamped by NIST+Google+Cloudflare
241
328
  ```
242
329
 
330
+ > **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.
331
+
243
332
  ---
244
333
 
245
334
  ### 3. DEX Protocol — Sandwich Deterrence
246
335
 
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.
336
+ **Solution**: Integrate `TTTHookSimple` (Uniswap V4 hook, Base Sepolia: `0x8C633b05b833a476925F7d9818da6E215760F2c7`). Honest builders get `turbo` mode. Tampered sequences get `full` mode (penalty delay). Economics, not governance.
337
+
338
+ > **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
339
 
249
340
  ---
250
341
 
@@ -252,19 +343,21 @@ const pot = await client.callTool({
252
343
 
253
344
  **Problem**: MiFIR Article 22c / RTS 25 requires microsecond-precision UTC-synchronized timestamps. Hardware PTP appliances cost $50K–$500K.
254
345
 
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.
346
+ **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
347
 
257
348
  ```typescript
258
349
  const audit = await client.callTool({
259
350
  name: "pot_generate",
260
351
  arguments: { txHash: tradeHash, chainId: 8453 }
261
352
  });
262
- // audit.timestamp: nanosecond precision
263
- // audit.uncertainty: ±ms bound (RTS 25 required field)
353
+ // audit.timestamp: high-resolution timestamp
354
+ // audit.uncertainty: ± bound (RTS 25 uncertainty field)
264
355
  // audit.confidence: fraction of sources that agreed
265
356
  ```
266
357
 
267
- **Outcome**: MiFIR-grade audit trail. IETF standardized via `draft-helmprotocol-tttps-00`.
358
+ > **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.
359
+
360
+ **Outcome**: Structurally compatible audit trail. IETF specification: `draft-helmprotocol-tttps`.
268
361
 
269
362
  ---
270
363
 
@@ -276,16 +369,48 @@ const audit = await client.callTool({
276
369
 
277
370
  ---
278
371
 
279
- ## Rate Limits & Pricing
372
+ ## How It Differs — A Different Job, Not "Better"
280
373
 
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
- ```
374
+ | Tool | Integration | What it recalls | Integrity | Hot-path cost |
375
+ |------|-------------|-----------------|-----------|---------------|
376
+ | Letta (MemGPT) | owns the agent loop | self-editing semantic memory | none | embedding + vector search per memory op |
377
+ | LangGraph / LangMem | LangGraph only | graph state / semantic | none | checkpoint I/O (+ embeddings) |
378
+ | RAG / vector DB | bolt-on | fuzzy similarity | none | embed + vector search per item |
379
+ | **ttt-mcp** | **2-min MCP retrofit** | **exact causal step (by eventId)** | **Ed25519 + TTTPS timestamp** | **sign + hash + write — 0 embedding calls** |
380
+
381
+ **The cost difference is structural, not incidental.**
382
+
383
+ 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.
384
+
385
+ **Scope**: agents stamp the steps worth checkpointing — not every token, not every query. Volume tracks decisions, not total chat traffic.
386
+
387
+ 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.
388
+
389
+ ---
390
+
391
+ ## Pricing
392
+
393
+ | Tier | Price | Calls/month |
394
+ |------|-------|-------------|
395
+ | Free | $0 | 100/day per IP — no signup |
396
+ | Dev | $29/mo | 100K |
397
+ | Pro | $99/mo | 1M |
398
+ | Team | $299/mo | 10M + $0.01/1K overage |
399
+ | Enterprise | $999+/mo | 100M calls/mo · $0.001/1K overage · SLA 99.9% |
400
+ | Platform License | Negotiated ($2M+/yr) | Volume cap negotiated · native integration |
401
+
402
+ **Subscribe:**
287
403
 
288
- Contact: heime.jorgen@proton.me
404
+ Dev **$29/mo** · Pro **$99/mo** · Team **$299/mo** — to subscribe, email [peter@kenosian.com](mailto:peter@kenosian.com).
405
+
406
+ Enterprise & Platform License: [peter@kenosian.com](mailto:peter@kenosian.com)
407
+
408
+ Contact: peter@kenosian.com
409
+
410
+ **Quota mechanics — stdio vs HTTP:**
411
+
412
+ - **HTTP mode** (Glama / Smithery container, `PORT` set): the per-IP free tier limit (100 calls/day) is enforced locally in the server process.
413
+ - **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
414
 
290
415
  ---
291
416
 
@@ -294,12 +419,37 @@ Contact: heime.jorgen@proton.me
294
419
  - Node.js >= 18
295
420
  - Network access for time synthesis (HTTPS to time.nist.gov, time.google.com, time.cloudflare.com)
296
421
 
422
+ **Time source tiers (automatic fallback):**
423
+
424
+ | Tier | Source | Stratum | Notes |
425
+ |------|--------|---------|-------|
426
+ | 1 (preferred) | PTP / hardware clock | 0–1 | Requires local PTP daemon |
427
+ | 2 | Roughtime / NTP (NIST, Google, Cloudflare) | 2–4 | Default for most deployments |
428
+ | 3 (offline fallback) | Local system clock | 16 | RFC 5905 unsynchronized stratum — used when all network sources are unreachable |
429
+
430
+ 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.
431
+
432
+ **Redis persistence (optional):**
433
+
434
+ 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.
435
+
436
+ ---
437
+
438
+ ## Production Tips
439
+
440
+ **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.
441
+
442
+ ```bash
443
+ # Kubernetes / Docker: add to your startup script
444
+ curl -s http://your-server/pot/health > /dev/null
445
+ ```
446
+
297
447
  ---
298
448
 
299
449
  ## Learn More
300
450
 
301
451
  - [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
452
+ - [IETF Draft: draft-helmprotocol-tttps](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) — TTTPS Protocol Specification
303
453
  - [Helm Protocol](https://github.com/Helm-Protocol) — GitHub
304
454
 
305
455
  ## 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,11 +61,50 @@ 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.1" });
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. Either eventId or txHash 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"),
@@ -72,12 +112,13 @@ function buildMcpServer() {
72
112
  chainId: import_zod.z.number().optional().describe("EVM chain ID (DeFi, e.g. 8453 for Base)"),
73
113
  poolAddress: import_zod.z.string().optional().describe("DEX pool contract address (DeFi)")
74
114
  },
115
+ { title: "Generate Proof of Time", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
75
116
  async (args) => {
76
117
  try {
77
118
  const result = await (0, import_tools.potGenerate)(args);
78
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
119
+ return toolSuccess(result);
79
120
  } catch (err) {
80
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
121
+ return toolError(err);
81
122
  }
82
123
  }
83
124
  );
@@ -90,12 +131,13 @@ function buildMcpServer() {
90
131
  chainId: import_zod.z.number().describe("EVM chain ID (e.g. 84532 for Base Sepolia)"),
91
132
  poolAddress: import_zod.z.string().describe("Uniswap V4 pool address (0x-prefixed)")
92
133
  },
134
+ { title: "Verify Proof of Time", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
93
135
  async (args) => {
94
136
  try {
95
137
  const result = await (0, import_tools.potVerify)(args);
96
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
138
+ return toolSuccess(result);
97
139
  } catch (err) {
98
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
140
+ return toolError(err);
99
141
  }
100
142
  }
101
143
  );
@@ -108,12 +150,13 @@ function buildMcpServer() {
108
150
  endTime: import_zod.z.number().optional().describe("End time (unix ms). Default: now"),
109
151
  limit: import_zod.z.number().optional().describe("Max entries to return. Default: 100, max: 1000")
110
152
  },
153
+ { title: "Query Proof of Time Records", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
111
154
  async (args) => {
112
155
  try {
113
156
  const result = await (0, import_tools.potQuery)(args);
114
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
157
+ return toolSuccess(result);
115
158
  } catch (err) {
116
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
159
+ return toolError(err);
117
160
  }
118
161
  }
119
162
  );
@@ -124,12 +167,13 @@ function buildMcpServer() {
124
167
  eventId: import_zod.z.string().describe("The workflow step to start traversal from"),
125
168
  depth: import_zod.z.number().optional().describe("Max backward traversal depth. Default: 10, max: 100")
126
169
  },
170
+ { title: "Traverse PoT Causal Chain", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
127
171
  async (args) => {
128
172
  try {
129
173
  const result = await (0, import_tools.potGraph)(args);
130
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
174
+ return toolSuccess(result);
131
175
  } catch (err) {
132
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
176
+ return toolError(err);
133
177
  }
134
178
  }
135
179
  );
@@ -137,12 +181,13 @@ function buildMcpServer() {
137
181
  "pot_stats",
138
182
  "Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given period.",
139
183
  { period: import_zod.z.enum(["day", "week", "month"]).describe("Time period for statistics") },
184
+ { title: "PoT Statistics", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
140
185
  async (args) => {
141
186
  try {
142
187
  const result = await (0, import_tools.potStats)(args);
143
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
188
+ return toolSuccess(result);
144
189
  } catch (err) {
145
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
190
+ return toolError(err);
146
191
  }
147
192
  }
148
193
  );
@@ -150,12 +195,13 @@ function buildMcpServer() {
150
195
  "pot_health",
151
196
  "Check PoT system health: time source status, subgraph sync, server uptime, and current mode.",
152
197
  {},
198
+ { title: "PoT System Health", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
153
199
  async () => {
154
200
  try {
155
201
  const result = await (0, import_tools.potHealth)();
156
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
202
+ return toolSuccess(result);
157
203
  } catch (err) {
158
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
204
+ return toolError(err);
159
205
  }
160
206
  }
161
207
  );
@@ -169,12 +215,13 @@ function buildMcpServer() {
169
215
  endTime: import_zod.z.number().optional().describe("Unix ms end time (optional, default: now)"),
170
216
  maxTokens: import_zod.z.number().optional().describe("Approximate max tokens for rollup (default: 2000)")
171
217
  },
218
+ { title: "Create PoT Checkpoint", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
172
219
  async (args) => {
173
220
  try {
174
221
  const result = await (0, import_tools.potCheckpoint)(args);
175
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
222
+ return toolSuccess(result);
176
223
  } catch (err) {
177
- return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
224
+ return toolError(err);
178
225
  }
179
226
  }
180
227
  );
@@ -187,7 +234,7 @@ async function main() {
187
234
  const httpServer = (0, import_http.createServer)(async (req, res) => {
188
235
  if (req.method === "GET" && (req.url === "/health" || req.url === "/ping")) {
189
236
  res.writeHead(200, { "Content-Type": "application/json" });
190
- res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.2.0" }));
237
+ res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.3.1" }));
191
238
  return;
192
239
  }
193
240
  if (req.method === "POST") {
@@ -204,7 +251,8 @@ async function main() {
204
251
  res.end(
205
252
  JSON.stringify({
206
253
  error: "rate_limit_exceeded",
207
- message: "Free tier: 100 calls/day reached. Contact heime.jorgen@proton.me for commercial access.",
254
+ message: import_server.FREE_TIER_UPGRADE_MESSAGE,
255
+ upgradeUrl: import_server.UPGRADE_URL,
208
256
  tier: "free"
209
257
  })
210
258
  );
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
@@ -36,16 +36,31 @@ __export(tools_exports, {
36
36
  potStats: () => potStats,
37
37
  potVerify: () => potVerify,
38
38
  redis: () => redis,
39
- restoreDagEntry: () => restoreDagEntry
39
+ restoreDagEntry: () => restoreDagEntry,
40
+ tttsFreshnessSeal: () => tttsFreshnessSeal,
41
+ updateLastPot: () => updateLastPot
40
42
  });
41
43
  module.exports = __toCommonJS(tools_exports);
42
44
  var import_openttt = require("openttt");
43
45
  var import_telemetry = require("./telemetry");
46
+ var import_server = require("./server");
44
47
  var import_ioredis = __toESM(require("ioredis"));
45
48
  const GrgPipeline = (
46
49
  // eslint-disable-next-line @typescript-eslint/no-require-imports
47
50
  require("openttt").GrgPipeline ?? null
48
51
  );
52
+ function applyAdvisory(result, advisory) {
53
+ if (!advisory) return result;
54
+ if (result === null || typeof result !== "object") return result;
55
+ const notices = [];
56
+ if (advisory.warning) notices.push(advisory.warning);
57
+ if (advisory.overageActive) notices.push("Overage billing is active \u2014 usage above your plan limit will be charged.");
58
+ if (notices.length === 0) return result;
59
+ return { ...result, _quotaNotice: notices.join(" | ") };
60
+ }
61
+ function resolvePaidApiKey() {
62
+ return process.env.TTT_API_KEY?.trim() || void 0;
63
+ }
49
64
  const redis = new import_ioredis.default(process.env.REDIS_URL ?? "redis://127.0.0.1:6379", {
50
65
  lazyConnect: true,
51
66
  enableOfflineQueue: false,
@@ -101,6 +116,21 @@ function depthThresholdFromTokens(maxTokens, entryCount) {
101
116
  rollup: Math.min(minimalDepth, entryCount)
102
117
  };
103
118
  }
119
+ let _lastPotTimestampNs = null;
120
+ let _lastPotStratum = 16;
121
+ let _lastPotSources = 0;
122
+ function updateLastPot(timestampNs, stratum, sources) {
123
+ _lastPotTimestampNs = timestampNs;
124
+ _lastPotStratum = stratum;
125
+ _lastPotSources = sources;
126
+ }
127
+ function tttsFreshnessSeal() {
128
+ if (_lastPotTimestampNs === null) return null;
129
+ const nowNs = BigInt(Date.now()) * 1000000n;
130
+ const age_ms = Number((nowNs - _lastPotTimestampNs) / 1000000n);
131
+ const ttlMs = _lastPotStratum <= 3 ? 100 : _lastPotStratum <= 8 ? 1e3 : _lastPotStratum <= 15 ? 5e3 : 0;
132
+ return { age_ms, stratum: _lastPotStratum, sources: _lastPotSources, ttlMs };
133
+ }
104
134
  function restoreDagEntry(entry) {
105
135
  if (potByEventId.has(entry.eventId)) return;
106
136
  const e = {
@@ -146,6 +176,22 @@ async function potGenerate(args) {
146
176
  throw new Error("Either eventId (Claude Code) or txHash (DeFi) is required");
147
177
  }
148
178
  (0, import_telemetry.telemetryIncrement)("pot_generate");
179
+ const apiKey = resolvePaidApiKey();
180
+ if (apiKey && args.eventId) {
181
+ const { data, advisory } = await (0, import_server.delegateToServer)({
182
+ apiKey,
183
+ method: "POST",
184
+ path: "/pot/generate",
185
+ body: { eventId: args.eventId, prevEventId: args.prevEventId }
186
+ });
187
+ if (data && typeof data === "object") {
188
+ const d = data;
189
+ if (typeof d.stratum === "number" && typeof d.sources === "number" && typeof d.timestamp === "string") {
190
+ updateLastPot(BigInt(d.timestamp), d.stratum, d.sources);
191
+ }
192
+ }
193
+ return applyAdvisory(data, advisory);
194
+ }
149
195
  let pot;
150
196
  let isOfflineFallback = false;
151
197
  try {
@@ -241,6 +287,7 @@ async function potGenerate(args) {
241
287
  ).catch(() => {
242
288
  });
243
289
  }
290
+ updateLastPot(pot.timestamp, pot.stratum, pot.sources);
244
291
  return serialize({
245
292
  potHash,
246
293
  eventId: args.eventId ?? null,
@@ -284,6 +331,21 @@ async function potVerify(args) {
284
331
  }
285
332
  async function potQuery(args) {
286
333
  (0, import_telemetry.telemetryIncrement)("pot_query");
334
+ const apiKey = resolvePaidApiKey();
335
+ if (apiKey) {
336
+ const { data, advisory } = await (0, import_server.delegateToServer)({
337
+ apiKey,
338
+ method: "GET",
339
+ path: "/pot/query",
340
+ query: {
341
+ eventId: args.eventId,
342
+ startTimeNs: args.startTime != null ? args.startTime * 1e6 : void 0,
343
+ endTimeNs: args.endTime != null ? args.endTime * 1e6 : void 0,
344
+ limit: args.limit
345
+ }
346
+ });
347
+ return applyAdvisory(data, advisory);
348
+ }
287
349
  if (args.eventId) {
288
350
  const entry = potByEventId.get(args.eventId);
289
351
  return serialize({
@@ -338,6 +400,16 @@ async function potQuery(args) {
338
400
  }
339
401
  async function potGraph(args) {
340
402
  (0, import_telemetry.telemetryIncrement)("pot_graph");
403
+ const apiKey = resolvePaidApiKey();
404
+ if (apiKey) {
405
+ const { data, advisory } = await (0, import_server.delegateToServer)({
406
+ apiKey,
407
+ method: "GET",
408
+ path: "/pot/graph",
409
+ query: { eventId: args.eventId, depth: args.depth }
410
+ });
411
+ return applyAdvisory(data, advisory);
412
+ }
341
413
  const maxDepth = Math.min(args.depth ?? 10, 100);
342
414
  const backwardChain = [];
343
415
  let cursor = potByEventId.get(args.eventId);
@@ -367,6 +439,15 @@ async function potGraph(args) {
367
439
  }
368
440
  async function potStats(args) {
369
441
  (0, import_telemetry.telemetryIncrement)("pot_stats");
442
+ const apiKey = resolvePaidApiKey();
443
+ if (apiKey) {
444
+ const { data, advisory } = await (0, import_server.delegateToServer)({
445
+ apiKey,
446
+ method: "GET",
447
+ path: "/pot/stats"
448
+ });
449
+ return applyAdvisory(data, advisory);
450
+ }
370
451
  const now = Date.now();
371
452
  const periodMs = {
372
453
  day: 864e5,
@@ -447,6 +528,22 @@ async function potHealth() {
447
528
  }
448
529
  async function potCheckpoint(args) {
449
530
  (0, import_telemetry.telemetryIncrement)("pot_checkpoint");
531
+ const apiKey = resolvePaidApiKey();
532
+ if (apiKey) {
533
+ const { data, advisory } = await (0, import_server.delegateToServer)({
534
+ apiKey,
535
+ method: "GET",
536
+ path: "/pot/checkpoint",
537
+ query: {
538
+ fromEventId: args.fromEventId,
539
+ toEventId: args.toEventId,
540
+ startTime: args.startTime,
541
+ endTime: args.endTime,
542
+ maxTokens: args.maxTokens
543
+ }
544
+ });
545
+ return applyAdvisory(data, advisory);
546
+ }
450
547
  const now = Date.now();
451
548
  const startTime = args.startTime ?? now - 36e5;
452
549
  const endTime = args.endTime ?? now;
@@ -470,7 +567,7 @@ async function potCheckpoint(args) {
470
567
  const depthThreshold = args.maxTokens ? depthThresholdFromTokens(args.maxTokens, entries.length) : void 0;
471
568
  const compressed = entries.map((e, i) => compressEntry(e, i + 1, depthThreshold));
472
569
  const chainIntact = !entries.some((e) => e.eventId && evictedEventIds.has(e.eventId));
473
- const nextCheckpointHint = Math.max(10, 100 - eventCount % 100);
570
+ const nextCheckpointHint = Math.max(10, 240 - eventCount % 240);
474
571
  const checkpointId = `ckpt_${now}_${eventCount}`;
475
572
  const firstTs = entries[0]?.timestamp ?? null;
476
573
  const lastTs = entries[entries.length - 1]?.timestamp ?? null;
@@ -494,5 +591,7 @@ async function potCheckpoint(args) {
494
591
  potStats,
495
592
  potVerify,
496
593
  redis,
497
- restoreDagEntry
594
+ restoreDagEntry,
595
+ tttsFreshnessSeal,
596
+ updateLastPot
498
597
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helm-protocol/ttt-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "MCP Server for OpenTTT — Proof of Time tools for AI agents",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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"