@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 +215 -39
- package/dist/auth.js +1 -1
- package/dist/index.js +90 -19
- package/dist/server.js +136 -0
- package/dist/tools.js +166 -6
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
# @helm-protocol/ttt-mcp
|
|
2
2
|
|
|
3
|
-
> Reference implementation of [draft-helmprotocol-tttps
|
|
3
|
+
> Reference implementation of [draft-helmprotocol-tttps](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) (IETF Experimental)
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
39
|
+
## Mathematical Guarantees
|
|
28
40
|
|
|
29
41
|
| Layer | Mechanism | Guarantee |
|
|
30
42
|
|-------|-----------|-----------|
|
|
31
|
-
| **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
### Cursor
|
|
83
|
+
|
|
84
|
+
[](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 |
|
|
61
|
-
|
|
62
|
-
| `pot_generate` | Stamp a workflow step with
|
|
63
|
-
| `pot_verify` | Verify a
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
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 `
|
|
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
|
-
- `
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
263
|
-
// audit.uncertainty: ±
|
|
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
|
-
**
|
|
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
|
-
##
|
|
398
|
+
## How It Differs — A Different Job, Not "Better"
|
|
280
399
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
123
|
+
return toolSuccess(result);
|
|
79
124
|
} catch (err) {
|
|
80
|
-
return
|
|
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
|
|
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
|
|
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
|
|
180
|
+
return toolSuccess(result);
|
|
115
181
|
} catch (err) {
|
|
116
|
-
return
|
|
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
|
|
197
|
+
return toolSuccess(result);
|
|
131
198
|
} catch (err) {
|
|
132
|
-
return
|
|
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
|
|
211
|
+
return toolSuccess(result);
|
|
144
212
|
} catch (err) {
|
|
145
|
-
return
|
|
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
|
|
225
|
+
return toolSuccess(result);
|
|
157
226
|
} catch (err) {
|
|
158
|
-
return
|
|
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
|
|
245
|
+
return toolSuccess(result);
|
|
176
246
|
} catch (err) {
|
|
177
|
-
return
|
|
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
|
|
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:
|
|
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("
|
|
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,
|
|
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.
|
|
4
|
-
"description": "
|
|
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.
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
55
56
|
"ioredis": "^5.11.0",
|
|
56
57
|
"openttt": "^0.2.13",
|
|
57
58
|
"zod": "^3.25.0"
|