@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 +187 -37
- package/dist/auth.js +1 -1
- package/dist/index.js +66 -18
- package/dist/server.js +136 -0
- package/dist/tools.js +102 -3
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
**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
|
-
|
|
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
|
|
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
|
|
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
|
|
27
|
+
## Mathematical Guarantees
|
|
28
28
|
|
|
29
29
|
| Layer | Mechanism | Guarantee |
|
|
30
30
|
|-------|-----------|-----------|
|
|
31
|
-
| **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
### Cursor
|
|
71
|
+
|
|
72
|
+
[](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 |
|
|
61
|
-
|
|
62
|
-
| `pot_generate` | Stamp a workflow step with
|
|
63
|
-
| `pot_verify` | Verify a
|
|
64
|
-
| `pot_query` | O(1) exact lookup by eventId —
|
|
65
|
-
| `pot_graph` | Traverse
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
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
|
-
- `
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
263
|
-
// audit.uncertainty: ±
|
|
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
|
-
**
|
|
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
|
-
##
|
|
372
|
+
## How It Differs — A Different Job, Not "Better"
|
|
280
373
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
119
|
+
return toolSuccess(result);
|
|
79
120
|
} catch (err) {
|
|
80
|
-
return
|
|
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
|
|
138
|
+
return toolSuccess(result);
|
|
97
139
|
} catch (err) {
|
|
98
|
-
return
|
|
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
|
|
157
|
+
return toolSuccess(result);
|
|
115
158
|
} catch (err) {
|
|
116
|
-
return
|
|
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
|
|
174
|
+
return toolSuccess(result);
|
|
131
175
|
} catch (err) {
|
|
132
|
-
return
|
|
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
|
|
188
|
+
return toolSuccess(result);
|
|
144
189
|
} catch (err) {
|
|
145
|
-
return
|
|
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
|
|
202
|
+
return toolSuccess(result);
|
|
157
203
|
} catch (err) {
|
|
158
|
-
return
|
|
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
|
|
222
|
+
return toolSuccess(result);
|
|
176
223
|
} catch (err) {
|
|
177
|
-
return
|
|
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.
|
|
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:
|
|
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,
|
|
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.
|
|
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.
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
55
56
|
"ioredis": "^5.11.0",
|
|
56
57
|
"openttt": "^0.2.13",
|
|
57
58
|
"zod": "^3.25.0"
|