@helm-protocol/ttt-mcp 0.2.1 → 0.3.0
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 +214 -99
- package/dist/auth.js +49 -11
- package/dist/index.js +173 -113
- package/dist/tools.js +277 -9
- package/package.json +23 -3
package/README.md
CHANGED
|
@@ -4,79 +4,115 @@
|
|
|
4
4
|
|
|
5
5
|
**MCP Server for OpenTTT — Proof of Time tools for AI agents**
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
> Who was first?
|
|
9
|
-
>
|
|
10
|
-
> OpenTTT answers this with cryptographic Proof of Time — synthesized from
|
|
11
|
-
> multiple independent time sources, verified through GRG integrity shards,
|
|
12
|
-
> and signed with Ed25519 for non-repudiation.
|
|
7
|
+
---
|
|
13
8
|
|
|
14
|
-
##
|
|
9
|
+
## The Problem: Workflow Amnesia
|
|
10
|
+
|
|
11
|
+
Large Claude Code workflows — 20-agent Dynamic Workflows, multi-day multi-session projects, 100K+ token contexts — all face the same failure mode: **context compression erases action history.**
|
|
12
|
+
|
|
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
|
+
|
|
15
|
+
**ttt-mcp is the external nervous system that survives context compression.**
|
|
16
|
+
|
|
17
|
+
Every workflow step is anchored to a cryptographic timestamp on an **external server** — physically separate from Claude's context window. When compression happens, agents query their exact action history through the MCP tools and resume with full causal context.
|
|
15
18
|
|
|
16
|
-
```bash
|
|
17
|
-
npm install @helm-protocol/ttt-mcp
|
|
18
19
|
```
|
|
20
|
+
Claude workflow → [context compressed] → agents call pot_query(eventId)
|
|
21
|
+
→ external server returns full timeline
|
|
22
|
+
→ workflow resumes, zero lost state
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Mathematical Guarantee
|
|
28
|
+
|
|
29
|
+
| Layer | Mechanism | Guarantee |
|
|
30
|
+
|-------|-----------|-----------|
|
|
31
|
+
| **Identity** | SHA-3 eventId (256-bit) | Collision probability 2⁻²⁵⁶ ≈ 0 — practically 100% exact step recall |
|
|
32
|
+
| **Ordering** | TTTPS causal timestamps | Total order on events — tamper-proof sequence proof |
|
|
33
|
+
| **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
|
+
| **Non-repudiation** | Ed25519 signature | Cryptographic proof of who acted when |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
19
40
|
|
|
41
|
+
```bash
|
|
42
|
+
# Claude Desktop
|
|
20
43
|
```json
|
|
21
|
-
// claude_desktop_config.json
|
|
22
44
|
{
|
|
23
45
|
"mcpServers": {
|
|
24
46
|
"ttt": {
|
|
25
47
|
"command": "npx",
|
|
26
|
-
"args": ["@helm-protocol/ttt-mcp"]
|
|
48
|
+
"args": ["-y", "@helm-protocol/ttt-mcp"]
|
|
27
49
|
}
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
```
|
|
31
53
|
|
|
32
|
-
|
|
54
|
+
Add `TTT_API_KEY` for unlimited calls (free tier: 100 calls/day per IP).
|
|
55
|
+
|
|
56
|
+
---
|
|
33
57
|
|
|
34
58
|
## Tools
|
|
35
59
|
|
|
36
60
|
| Tool | Description |
|
|
37
61
|
|------|-------------|
|
|
38
|
-
| `pot_generate` |
|
|
39
|
-
| `pot_verify` | Verify a Proof of Time using its hash and
|
|
40
|
-
| `pot_query` |
|
|
62
|
+
| `pot_generate` | Stamp a workflow step with eventId + prevEventId (builds causal chain) |
|
|
63
|
+
| `pot_verify` | Verify a Proof of Time using its hash and integrity shards |
|
|
64
|
+
| `pot_query` | O(1) exact lookup by eventId — call this after context compression |
|
|
65
|
+
| `pot_graph` | Traverse full causal DAG — backward + forward chain from any step |
|
|
41
66
|
| `pot_stats` | Get turbo/full mode statistics for a time period |
|
|
42
|
-
| `pot_health` | Check system health: time sources,
|
|
67
|
+
| `pot_health` | Check system health: time sources, uptime, current mode |
|
|
68
|
+
| `pot_checkpoint` | Create a compressed rollup checkpoint of workflow history |
|
|
69
|
+
|
|
70
|
+
---
|
|
43
71
|
|
|
44
72
|
## Tool Parameters
|
|
45
73
|
|
|
46
74
|
### pot_generate
|
|
47
75
|
|
|
48
|
-
|
|
76
|
+
Stamp a workflow step with a cryptographic timestamp. For Claude Code: use `eventId` + `prevEventId`. For DeFi: use `txHash` + `chainId` + `poolAddress`. One of `eventId` or `txHash` is required.
|
|
49
77
|
|
|
50
78
|
| Parameter | Type | Required | Description |
|
|
51
79
|
|-----------|------|----------|-------------|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
80
|
+
| eventId | string | Either/or | Workflow step identifier. E.g. `"refactor_auth_step1"` |
|
|
81
|
+
| prevEventId | string | No | Previous step's eventId — links steps into a causal chain |
|
|
82
|
+
| txHash | string | Either/or | Transaction hash (DeFi, hex with 0x prefix) |
|
|
83
|
+
| chainId | number | No | EVM chain ID (DeFi) |
|
|
84
|
+
| poolAddress | string | No | DEX pool contract address (DeFi) |
|
|
55
85
|
|
|
56
|
-
###
|
|
86
|
+
### pot_query
|
|
57
87
|
|
|
58
|
-
|
|
88
|
+
Query Proof of Time records. Use `eventId` for O(1) exact lookup after context compression.
|
|
59
89
|
|
|
60
90
|
| Parameter | Type | Required | Description |
|
|
61
91
|
|-----------|------|----------|-------------|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
92
|
+
| eventId | string | No | Exact step lookup — collision probability 2⁻²⁵⁶ |
|
|
93
|
+
| startTime | number | No | Start time (unix ms). Default: 24h ago |
|
|
94
|
+
| endTime | number | No | End time (unix ms). Default: now |
|
|
95
|
+
| limit | number | No | Max entries to return. Default: 100, max: 1000 |
|
|
66
96
|
|
|
67
|
-
###
|
|
97
|
+
### pot_graph
|
|
68
98
|
|
|
69
|
-
|
|
99
|
+
Traverse the causal chain from any step. Returns backward chain (ancestors) and forward chain (descendants).
|
|
70
100
|
|
|
71
101
|
| Parameter | Type | Required | Description |
|
|
72
102
|
|-----------|------|----------|-------------|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
| limit | number | No | Max entries to return. Default: 100, max: 1000 |
|
|
103
|
+
| eventId | string | Yes | Step to traverse from |
|
|
104
|
+
| depth | number | No | Max backward depth. Default: 10, max: 100 |
|
|
76
105
|
|
|
77
|
-
###
|
|
106
|
+
### pot_verify
|
|
107
|
+
|
|
108
|
+
| Parameter | Type | Required | Description |
|
|
109
|
+
|-----------|------|----------|-------------|
|
|
110
|
+
| potHash | string | Yes | PoT hash to verify (hex with 0x prefix) |
|
|
111
|
+
| grgShards | string[] | Yes | Array of hex-encoded cryptographic integrity shards |
|
|
112
|
+
| chainId | number | Yes | EVM chain ID |
|
|
113
|
+
| poolAddress | string | Yes | Uniswap V4 pool address |
|
|
78
114
|
|
|
79
|
-
|
|
115
|
+
### pot_stats
|
|
80
116
|
|
|
81
117
|
| Parameter | Type | Required | Description |
|
|
82
118
|
|-----------|------|----------|-------------|
|
|
@@ -84,107 +120,182 @@ Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given
|
|
|
84
120
|
|
|
85
121
|
### pot_health
|
|
86
122
|
|
|
87
|
-
|
|
123
|
+
No parameters.
|
|
124
|
+
|
|
125
|
+
### pot_checkpoint
|
|
126
|
+
|
|
127
|
+
Creates a compressed rollup checkpoint of workflow history.
|
|
128
|
+
|
|
129
|
+
**Use when:** Approaching context limit, before long tasks, or every ~100 events.
|
|
88
130
|
|
|
89
131
|
| Parameter | Type | Required | Description |
|
|
90
132
|
|-----------|------|----------|-------------|
|
|
91
|
-
|
|
|
133
|
+
| fromEventId | string | No | Start of range — first eventId in the causal chain to include |
|
|
134
|
+
| toEventId | string | No | End of range — last eventId in the causal chain to include |
|
|
135
|
+
| startTime | number | No | Unix ms. Default: 1 hour ago |
|
|
136
|
+
| endTime | number | No | Unix ms. Default: now |
|
|
137
|
+
| maxTokens | number | No | Approximate max tokens for rollup output. Default: 2000 |
|
|
92
138
|
|
|
93
|
-
|
|
139
|
+
**Returns:**
|
|
140
|
+
- `checkpointId` — unique checkpoint identifier
|
|
141
|
+
- `rollupSummary` — compressed event history (depth-adaptive: full/compact/minimal/rollup)
|
|
142
|
+
- `chainIntact` — whether the causal chain is unbroken
|
|
143
|
+
- `nextCheckpointHint` — recommended events before next checkpoint
|
|
144
|
+
|
|
145
|
+
**Depth-adaptive compression:**
|
|
146
|
+
|
|
147
|
+
| Depth | Format | ~Tokens |
|
|
148
|
+
|-------|--------|---------|
|
|
149
|
+
| 1–5 | Full entry | ~200/event |
|
|
150
|
+
| 6–20 | Compact (id+hash+ts) | ~80/event |
|
|
151
|
+
| 21–50 | Minimal (id+ts) | ~30/event |
|
|
152
|
+
| 51+ | Rollup string | ~10/event |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Use Cases
|
|
157
|
+
|
|
158
|
+
### 1. Claude Code Workflow — Amnesia Prevention
|
|
159
|
+
|
|
160
|
+
**Problem**: A 20-agent Dynamic Workflow refactors a 500K-line codebase over hours. After each context compression, agents have no memory of what they already processed. Duplicate work. State corruption.
|
|
161
|
+
|
|
162
|
+
**Solution**: Each agent stamps its steps with `pot_generate(eventId, prevEventId)`. After compression, it calls `pot_query(eventId)` to recover its exact action history — what ran, when, in what order — from the external server. The server is outside Claude's context window; compression never touches it.
|
|
94
163
|
|
|
95
164
|
```typescript
|
|
96
|
-
//
|
|
97
|
-
const pot = await
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
165
|
+
// Agent starts a workflow step
|
|
166
|
+
const pot = await client.callTool({
|
|
167
|
+
name: "pot_generate",
|
|
168
|
+
arguments: {
|
|
169
|
+
eventId: "refactor_auth_module_step3",
|
|
170
|
+
prevEventId: "refactor_auth_module_step2"
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// pot.potHash — cryptographic proof this step happened at this time
|
|
174
|
+
|
|
175
|
+
// After context compression, agent recovers its history:
|
|
176
|
+
const history = await client.callTool({
|
|
177
|
+
name: "pot_query",
|
|
178
|
+
arguments: { eventId: "refactor_auth_module_step3" }
|
|
179
|
+
});
|
|
180
|
+
// history.local[0] — exact record: timestamp, prevEventId, potHash
|
|
181
|
+
// history.found: true — O(1) lookup, collision probability 2⁻²⁵⁶
|
|
182
|
+
|
|
183
|
+
// Traverse full causal chain:
|
|
184
|
+
const chain = await client.callTool({
|
|
185
|
+
name: "pot_graph",
|
|
186
|
+
arguments: { eventId: "refactor_auth_module_step3", depth: 20 }
|
|
101
187
|
});
|
|
188
|
+
// chain.backwardChain — all ancestor steps in chronological order
|
|
189
|
+
// chain.forwardChain — steps that follow this one
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Before a long task or every ~100 events — create a checkpoint:**
|
|
102
193
|
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
194
|
+
```typescript
|
|
195
|
+
// Compress workflow history before context fills up — by causal range:
|
|
196
|
+
const checkpoint = await client.callTool({
|
|
197
|
+
name: "pot_checkpoint",
|
|
198
|
+
arguments: {
|
|
199
|
+
fromEventId: "refactor_auth_module_step1",
|
|
200
|
+
toEventId: "refactor_auth_module_step3"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// checkpoint.checkpointId — store this; resume from it after compression
|
|
204
|
+
// checkpoint.rollupSummary — depth-adaptive compressed history (10–200 tokens/event)
|
|
205
|
+
// checkpoint.chainIntact: true — causal chain verified unbroken
|
|
206
|
+
// checkpoint.nextCheckpointHint: 87 — suggested events before next checkpoint
|
|
207
|
+
|
|
208
|
+
// Or compress by time window with a token budget:
|
|
209
|
+
const checkpoint = await client.callTool({
|
|
210
|
+
name: "pot_checkpoint",
|
|
211
|
+
arguments: {
|
|
212
|
+
startTime: Date.now() - 3_600_000, // last 1 hour
|
|
213
|
+
maxTokens: 1500
|
|
214
|
+
}
|
|
215
|
+
});
|
|
107
216
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
217
|
+
// After context compression, restore from checkpoint instead of re-querying all events:
|
|
218
|
+
const history = await client.callTool({
|
|
219
|
+
name: "pot_query",
|
|
220
|
+
arguments: { eventId: checkpoint.checkpointId }
|
|
111
221
|
});
|
|
112
|
-
//
|
|
222
|
+
// Full causal context restored in a single call
|
|
113
223
|
```
|
|
114
224
|
|
|
115
|
-
|
|
225
|
+
**Outcome**: Zero duplicate work. Full workflow timeline recoverable even after complete context resets.
|
|
116
226
|
|
|
117
|
-
|
|
118
|
-
2. **GRG Pipeline** — Encodes transaction data through a multi-layer integrity pipeline, producing verifiable shards
|
|
119
|
-
3. **Ed25519 Signing** — Signs the PoT hash for non-repudiation
|
|
120
|
-
4. **Adaptive Mode** — Honest builders get `turbo` mode (fast, profitable); tampered sequences get `full` mode (slow, costly) — natural economic selection
|
|
227
|
+
---
|
|
121
228
|
|
|
122
|
-
|
|
229
|
+
### 2. MEV Bot — Transaction Ordering Proof
|
|
123
230
|
|
|
124
|
-
|
|
231
|
+
**Problem**: You got front-run. You can't prove it — mempool timestamps are per-node, unsigned, non-authoritative.
|
|
125
232
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
233
|
+
**Solution**: Call `pot_generate` before every submission. The PoT receipt is cryptographically signed by three independent time sources (NIST, Google, Cloudflare), anchored on Base Sepolia TTT ERC-1155. If front-running occurs, you have a timestamped, on-chain record predating the attacker's block inclusion.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const pot = await client.callTool({
|
|
237
|
+
name: "pot_generate",
|
|
238
|
+
arguments: { txHash: pendingTxHash, chainId: 8453, poolAddress: "0x..." }
|
|
239
|
+
});
|
|
240
|
+
// pot.potHash — your evidence, timestamped by NIST+Google+Cloudflare
|
|
135
241
|
```
|
|
136
242
|
|
|
137
|
-
|
|
138
|
-
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
139
|
-
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
140
|
-
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
243
|
+
---
|
|
141
244
|
|
|
142
|
-
|
|
245
|
+
### 3. DEX Protocol — Sandwich Deterrence
|
|
143
246
|
|
|
144
|
-
|
|
145
|
-
- Network access for time synthesis (HTTPS to time.nist.gov, time.google.com, time.cloudflare.com)
|
|
247
|
+
**Solution**: Integrate `TTTHookSimple` (Uniswap V4 hook, Base Sepolia: `0x8C633b05b833a476925F7d9818da6E215760F2c7`). Honest builders get `turbo` mode. Tampered sequences get `full` mode (exponential backoff). Economics, not governance.
|
|
146
248
|
|
|
147
|
-
|
|
249
|
+
---
|
|
148
250
|
|
|
149
|
-
|
|
150
|
-
|
|
251
|
+
### 4. Hedge Fund / Prop Desk — MiFIR Art.22c Compliance
|
|
252
|
+
|
|
253
|
+
**Problem**: MiFIR Article 22c / RTS 25 requires microsecond-precision UTC-synchronized timestamps. Hardware PTP appliances cost $50K–$500K.
|
|
254
|
+
|
|
255
|
+
**Solution**: `pot_generate` produces an Ed25519-signed timestamp with uncertainty bound and multi-source attestation. Structurally compatible with RTS 25 audit record requirements. One API call per trade.
|
|
151
256
|
|
|
152
|
-
|
|
153
|
-
const
|
|
257
|
+
```typescript
|
|
258
|
+
const audit = await client.callTool({
|
|
154
259
|
name: "pot_generate",
|
|
155
|
-
arguments: {
|
|
156
|
-
txHash: "0xabc123...",
|
|
157
|
-
chainId: 8453,
|
|
158
|
-
poolAddress: "0xdef456..."
|
|
159
|
-
}
|
|
260
|
+
arguments: { txHash: tradeHash, chainId: 8453 }
|
|
160
261
|
});
|
|
161
|
-
//
|
|
262
|
+
// audit.timestamp: nanosecond precision
|
|
263
|
+
// audit.uncertainty: ±ms bound (RTS 25 required field)
|
|
264
|
+
// audit.confidence: fraction of sources that agreed
|
|
162
265
|
```
|
|
163
266
|
|
|
164
|
-
|
|
267
|
+
**Outcome**: MiFIR-grade audit trail. IETF standardized via `draft-helmprotocol-tttps-00`.
|
|
165
268
|
|
|
166
|
-
|
|
167
|
-
import subprocess, json
|
|
269
|
+
---
|
|
168
270
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
capture_output=True, text=True
|
|
177
|
-
)
|
|
178
|
-
```
|
|
271
|
+
### 5. Multi-Agent Coordination — Causal Order Proof
|
|
272
|
+
|
|
273
|
+
**Problem**: When multiple AI agents interact in a pipeline, the causal order matters for debugging and audit. Agent logs are unverifiable.
|
|
274
|
+
|
|
275
|
+
**Solution**: Each agent stamps its action with `pot_generate`. The potHash chain is independently verifiable. `pot_graph` reconstructs who did what and in what order.
|
|
276
|
+
|
|
277
|
+
---
|
|
179
278
|
|
|
180
279
|
## Rate Limits & Pricing
|
|
181
280
|
|
|
182
281
|
```
|
|
183
282
|
Free Tier: 100 calls/day per IP — no API key needed
|
|
184
|
-
|
|
185
|
-
|
|
283
|
+
BOT Tier: $199/mo — unlimited, SLA
|
|
284
|
+
DEX Tier: $499/mo — unlimited, priority support
|
|
285
|
+
FUND Tier: $2K+/mo — enterprise, dedicated infra
|
|
186
286
|
```
|
|
187
287
|
|
|
288
|
+
Contact: heime.jorgen@proton.me
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Requirements
|
|
293
|
+
|
|
294
|
+
- Node.js >= 18
|
|
295
|
+
- Network access for time synthesis (HTTPS to time.nist.gov, time.google.com, time.cloudflare.com)
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
188
299
|
## Learn More
|
|
189
300
|
|
|
190
301
|
- [OpenTTT SDK](https://www.npmjs.com/package/openttt) — The underlying SDK
|
|
@@ -193,4 +304,8 @@ Commercial: peter@kenosian.com (hedge funds, DEX protocols, OTC desks)
|
|
|
193
304
|
|
|
194
305
|
## License
|
|
195
306
|
|
|
196
|
-
BSL-1.1
|
|
307
|
+
BSL-1.1 — free for non-commercial use.
|
|
308
|
+
|
|
309
|
+
**Commercial use** (production bots, hedge funds, prop desks) requires a license.
|
|
310
|
+
|
|
311
|
+
Change Date: 2029-05-28 → Apache 2.0
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,14 +17,28 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var auth_exports = {};
|
|
20
30
|
__export(auth_exports, {
|
|
31
|
+
FREE_TIER_LIMIT: () => FREE_TIER_LIMIT,
|
|
21
32
|
checkRateLimit: () => checkRateLimit,
|
|
22
33
|
resolveApiKey: () => resolveApiKey
|
|
23
34
|
});
|
|
24
35
|
module.exports = __toCommonJS(auth_exports);
|
|
36
|
+
var fs = __toESM(require("fs"));
|
|
37
|
+
var path = __toESM(require("path"));
|
|
38
|
+
var os = __toESM(require("os"));
|
|
25
39
|
const FREE_TIER_LIMIT = parseInt(process.env.FREE_TIER_LIMIT ?? "100", 10);
|
|
40
|
+
const USAGE_DIR = path.join(os.homedir(), ".ttt-mcp");
|
|
41
|
+
const USAGE_FILE = path.join(USAGE_DIR, "usage.json");
|
|
26
42
|
const buckets = /* @__PURE__ */ new Map();
|
|
27
43
|
function nextMidnightUtc() {
|
|
28
44
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -31,36 +47,58 @@ function nextMidnightUtc() {
|
|
|
31
47
|
);
|
|
32
48
|
return midnight.getTime();
|
|
33
49
|
}
|
|
50
|
+
function readUsageFile() {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(USAGE_DIR)) fs.mkdirSync(USAGE_DIR, { recursive: true });
|
|
53
|
+
if (fs.existsSync(USAGE_FILE)) {
|
|
54
|
+
return JSON.parse(fs.readFileSync(USAGE_FILE, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return { count: 0, resetAt: nextMidnightUtc() };
|
|
59
|
+
}
|
|
60
|
+
function writeUsageFile(entry) {
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(USAGE_DIR)) fs.mkdirSync(USAGE_DIR, { recursive: true });
|
|
63
|
+
fs.writeFileSync(USAGE_FILE, JSON.stringify(entry), "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
}
|
|
34
67
|
function checkRateLimit(apiKey, clientIp) {
|
|
35
68
|
if (apiKey && apiKey.trim().length > 0) {
|
|
36
69
|
return { allowed: true, remaining: -1, tier: "paid" };
|
|
37
70
|
}
|
|
38
|
-
const bucketKey = `ip:${clientIp}`;
|
|
39
71
|
const now = Date.now();
|
|
72
|
+
if (clientIp === "stdio") {
|
|
73
|
+
let entry2 = readUsageFile();
|
|
74
|
+
if (now >= entry2.resetAt) {
|
|
75
|
+
entry2 = { count: 0, resetAt: nextMidnightUtc() };
|
|
76
|
+
}
|
|
77
|
+
if (entry2.count >= FREE_TIER_LIMIT) {
|
|
78
|
+
return { allowed: false, remaining: 0, tier: "free" };
|
|
79
|
+
}
|
|
80
|
+
entry2.count += 1;
|
|
81
|
+
writeUsageFile(entry2);
|
|
82
|
+
return { allowed: true, remaining: FREE_TIER_LIMIT - entry2.count, tier: "free" };
|
|
83
|
+
}
|
|
84
|
+
const bucketKey = `ip:${clientIp}`;
|
|
40
85
|
let entry = buckets.get(bucketKey);
|
|
41
86
|
if (!entry || now >= entry.resetAt) {
|
|
42
87
|
entry = { count: 0, resetAt: nextMidnightUtc() };
|
|
43
88
|
buckets.set(bucketKey, entry);
|
|
44
89
|
}
|
|
45
90
|
if (entry.count >= FREE_TIER_LIMIT) {
|
|
46
|
-
return {
|
|
47
|
-
allowed: false,
|
|
48
|
-
remaining: 0,
|
|
49
|
-
tier: "free"
|
|
50
|
-
};
|
|
91
|
+
return { allowed: false, remaining: 0, tier: "free" };
|
|
51
92
|
}
|
|
52
93
|
entry.count += 1;
|
|
53
|
-
return {
|
|
54
|
-
allowed: true,
|
|
55
|
-
remaining: FREE_TIER_LIMIT - entry.count,
|
|
56
|
-
tier: "free"
|
|
57
|
-
};
|
|
94
|
+
return { allowed: true, remaining: FREE_TIER_LIMIT - entry.count, tier: "free" };
|
|
58
95
|
}
|
|
59
96
|
function resolveApiKey(headerValue) {
|
|
60
97
|
return headerValue?.trim() || process.env.TTT_API_KEY?.trim() || void 0;
|
|
61
98
|
}
|
|
62
99
|
// Annotate the CommonJS export names for ESM import in node:
|
|
63
100
|
0 && (module.exports = {
|
|
101
|
+
FREE_TIER_LIMIT,
|
|
64
102
|
checkRateLimit,
|
|
65
103
|
resolveApiKey
|
|
66
104
|
});
|
package/dist/index.js
CHANGED
|
@@ -7,127 +7,183 @@ 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
txHash: import_zod.z.string().describe("Transaction hash (hex with 0x prefix)"),
|
|
19
|
-
chainId: import_zod.z.number().describe("Chain ID (e.g. 8453 for Base, 84532 for Base Sepolia)"),
|
|
20
|
-
poolAddress: import_zod.z.string().describe("DEX pool contract address")
|
|
21
|
-
},
|
|
22
|
-
async (args) => {
|
|
23
|
-
try {
|
|
24
|
-
const result = await (0, import_tools.potGenerate)(args);
|
|
25
|
-
return {
|
|
26
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
27
|
-
};
|
|
28
|
-
} catch (err) {
|
|
29
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
-
return {
|
|
31
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
32
|
-
isError: true
|
|
33
|
-
};
|
|
34
|
-
}
|
|
10
|
+
async function restoreDAGFromRedis() {
|
|
11
|
+
try {
|
|
12
|
+
await Promise.race([
|
|
13
|
+
import_tools.redis.connect(),
|
|
14
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
15
|
+
]);
|
|
16
|
+
} catch {
|
|
17
|
+
return 0;
|
|
35
18
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
isError: true
|
|
57
|
-
};
|
|
58
|
-
}
|
|
19
|
+
let cursor = "0";
|
|
20
|
+
let restored = 0;
|
|
21
|
+
const entries = [];
|
|
22
|
+
try {
|
|
23
|
+
do {
|
|
24
|
+
const [next, keys] = await import_tools.redis.scan(cursor, "MATCH", "dag:*", "COUNT", "200");
|
|
25
|
+
cursor = next;
|
|
26
|
+
if (keys.length === 0) continue;
|
|
27
|
+
const values = await import_tools.redis.mget(...keys);
|
|
28
|
+
for (const v of values) {
|
|
29
|
+
if (!v) continue;
|
|
30
|
+
try {
|
|
31
|
+
const entry = JSON.parse(v);
|
|
32
|
+
if (entry.eventId) entries.push(entry);
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} while (cursor !== "0");
|
|
37
|
+
} catch {
|
|
38
|
+
return 0;
|
|
59
39
|
}
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
"pot_query",
|
|
63
|
-
"Query Proof of Time history from local log and on-chain subgraph.",
|
|
64
|
-
{
|
|
65
|
-
startTime: import_zod.z.number().optional().describe("Start time (unix ms). Default: 24h ago"),
|
|
66
|
-
endTime: import_zod.z.number().optional().describe("End time (unix ms). Default: now"),
|
|
67
|
-
limit: import_zod.z.number().optional().describe("Max entries to return. Default: 100, max: 1000")
|
|
68
|
-
},
|
|
69
|
-
async (args) => {
|
|
40
|
+
entries.sort((a, b) => a.createdAt - b.createdAt);
|
|
41
|
+
for (const e of entries) {
|
|
70
42
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
43
|
+
(0, import_tools.restoreDagEntry)({
|
|
44
|
+
eventId: e.eventId,
|
|
45
|
+
prevEventId: e.prevEventId,
|
|
46
|
+
potHash: e.potHash,
|
|
47
|
+
timestamp: e.timestamp,
|
|
48
|
+
stratum: e.stratum,
|
|
49
|
+
mode: e.mode,
|
|
50
|
+
createdAt: e.createdAt,
|
|
51
|
+
chainId: e.chainId,
|
|
52
|
+
poolAddress: e.poolAddress
|
|
53
|
+
});
|
|
54
|
+
restored++;
|
|
55
|
+
} catch {
|
|
81
56
|
}
|
|
82
57
|
}
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
"pot_stats",
|
|
86
|
-
"Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given period.",
|
|
87
|
-
{
|
|
88
|
-
period: import_zod.z.enum(["day", "week", "month"]).describe("Time period for statistics")
|
|
89
|
-
},
|
|
90
|
-
async (args) => {
|
|
91
|
-
try {
|
|
92
|
-
const result = await (0, import_tools.potStats)(args);
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
95
|
-
};
|
|
96
|
-
} catch (err) {
|
|
97
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
-
return {
|
|
99
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
100
|
-
isError: true
|
|
101
|
-
};
|
|
102
|
-
}
|
|
58
|
+
if (restored > 0) {
|
|
59
|
+
console.error(`[ttt-mcp] DAG restored from Redis: ${restored} entries`);
|
|
103
60
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
61
|
+
return restored;
|
|
62
|
+
}
|
|
63
|
+
function buildMcpServer() {
|
|
64
|
+
const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.1.0" });
|
|
65
|
+
s.tool(
|
|
66
|
+
"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.",
|
|
68
|
+
{
|
|
69
|
+
eventId: import_zod.z.string().optional().describe("Workflow step identifier (Claude Code). E.g. 'refactor_auth_step1'"),
|
|
70
|
+
prevEventId: import_zod.z.string().optional().describe("Previous step's eventId \u2014 links steps into a causal chain"),
|
|
71
|
+
txHash: import_zod.z.string().optional().describe("Transaction hash (DeFi, hex with 0x prefix)"),
|
|
72
|
+
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)")
|
|
74
|
+
},
|
|
75
|
+
async (args) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await (0, import_tools.potGenerate)(args);
|
|
78
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
81
|
+
}
|
|
121
82
|
}
|
|
122
|
-
|
|
123
|
-
|
|
83
|
+
);
|
|
84
|
+
s.tool(
|
|
85
|
+
"pot_verify",
|
|
86
|
+
"Verify a Proof of Time using its hash and integrity shards. Returns validity, mode (turbo/full), and timestamp.",
|
|
87
|
+
{
|
|
88
|
+
potHash: import_zod.z.string().describe("PoT hash to verify (hex with 0x prefix)"),
|
|
89
|
+
grgShards: import_zod.z.array(import_zod.z.string()).describe("Array of hex-encoded cryptographic integrity shards"),
|
|
90
|
+
chainId: import_zod.z.number().describe("EVM chain ID (e.g. 84532 for Base Sepolia)"),
|
|
91
|
+
poolAddress: import_zod.z.string().describe("Uniswap V4 pool address (0x-prefixed)")
|
|
92
|
+
},
|
|
93
|
+
async (args) => {
|
|
94
|
+
try {
|
|
95
|
+
const result = await (0, import_tools.potVerify)(args);
|
|
96
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
s.tool(
|
|
103
|
+
"pot_query",
|
|
104
|
+
"Query Proof of Time records. Use eventId for exact O(1) lookup of a specific workflow step (collision probability 2^-256). Use startTime/endTime for time-range queries.",
|
|
105
|
+
{
|
|
106
|
+
eventId: import_zod.z.string().optional().describe("Exact eventId lookup \u2014 call this at workflow start to restore action history after context compression"),
|
|
107
|
+
startTime: import_zod.z.number().optional().describe("Start time (unix ms). Default: 24h ago"),
|
|
108
|
+
endTime: import_zod.z.number().optional().describe("End time (unix ms). Default: now"),
|
|
109
|
+
limit: import_zod.z.number().optional().describe("Max entries to return. Default: 100, max: 1000")
|
|
110
|
+
},
|
|
111
|
+
async (args) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = await (0, import_tools.potQuery)(args);
|
|
114
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
s.tool(
|
|
121
|
+
"pot_graph",
|
|
122
|
+
"Traverse the causal chain of workflow steps. Given an eventId, returns the full backward chain (ancestors via prevEventId) and forward chain (steps that follow). Use after context compression to reconstruct the complete workflow timeline.",
|
|
123
|
+
{
|
|
124
|
+
eventId: import_zod.z.string().describe("The workflow step to start traversal from"),
|
|
125
|
+
depth: import_zod.z.number().optional().describe("Max backward traversal depth. Default: 10, max: 100")
|
|
126
|
+
},
|
|
127
|
+
async (args) => {
|
|
128
|
+
try {
|
|
129
|
+
const result = await (0, import_tools.potGraph)(args);
|
|
130
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
s.tool(
|
|
137
|
+
"pot_stats",
|
|
138
|
+
"Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given period.",
|
|
139
|
+
{ period: import_zod.z.enum(["day", "week", "month"]).describe("Time period for statistics") },
|
|
140
|
+
async (args) => {
|
|
141
|
+
try {
|
|
142
|
+
const result = await (0, import_tools.potStats)(args);
|
|
143
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
s.tool(
|
|
150
|
+
"pot_health",
|
|
151
|
+
"Check PoT system health: time source status, subgraph sync, server uptime, and current mode.",
|
|
152
|
+
{},
|
|
153
|
+
async () => {
|
|
154
|
+
try {
|
|
155
|
+
const result = await (0, import_tools.potHealth)();
|
|
156
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
s.tool(
|
|
163
|
+
"pot_checkpoint",
|
|
164
|
+
"Create a compressed rollup checkpoint of workflow history. Call this periodically to prevent token explosion when recovering from context compression. Returns checkpointId, compressed event history, chainIntact status, and nextCheckpointHint (recommended events before next checkpoint).",
|
|
165
|
+
{
|
|
166
|
+
fromEventId: import_zod.z.string().optional().describe("Start of range by eventId (optional, use with toEventId)"),
|
|
167
|
+
toEventId: import_zod.z.string().optional().describe("End of range by eventId (optional, use with fromEventId)"),
|
|
168
|
+
startTime: import_zod.z.number().optional().describe("Unix ms start time (optional, default: 1h ago)"),
|
|
169
|
+
endTime: import_zod.z.number().optional().describe("Unix ms end time (optional, default: now)"),
|
|
170
|
+
maxTokens: import_zod.z.number().optional().describe("Approximate max tokens for rollup (default: 2000)")
|
|
171
|
+
},
|
|
172
|
+
async (args) => {
|
|
173
|
+
try {
|
|
174
|
+
const result = await (0, import_tools.potCheckpoint)(args);
|
|
175
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
124
183
|
async function main() {
|
|
184
|
+
await restoreDAGFromRedis();
|
|
125
185
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
|
|
126
186
|
if (port) {
|
|
127
|
-
const transport = new import_streamableHttp.StreamableHTTPServerTransport({
|
|
128
|
-
sessionIdGenerator: void 0
|
|
129
|
-
});
|
|
130
|
-
await server.connect(transport);
|
|
131
187
|
const httpServer = (0, import_http.createServer)(async (req, res) => {
|
|
132
188
|
if (req.method === "GET" && (req.url === "/health" || req.url === "/ping")) {
|
|
133
189
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -148,7 +204,7 @@ async function main() {
|
|
|
148
204
|
res.end(
|
|
149
205
|
JSON.stringify({
|
|
150
206
|
error: "rate_limit_exceeded",
|
|
151
|
-
message: "Free tier
|
|
207
|
+
message: "Free tier: 100 calls/day reached. Contact heime.jorgen@proton.me for commercial access.",
|
|
152
208
|
tier: "free"
|
|
153
209
|
})
|
|
154
210
|
);
|
|
@@ -172,6 +228,9 @@ async function main() {
|
|
|
172
228
|
}
|
|
173
229
|
}
|
|
174
230
|
try {
|
|
231
|
+
const reqServer = buildMcpServer();
|
|
232
|
+
const transport = new import_streamableHttp.StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
233
|
+
await reqServer.connect(transport);
|
|
175
234
|
await transport.handleRequest(req, res);
|
|
176
235
|
} catch (err) {
|
|
177
236
|
if (!res.headersSent) {
|
|
@@ -184,8 +243,9 @@ async function main() {
|
|
|
184
243
|
console.error(`[ttt-mcp] OpenTTT MCP Server (HTTP) on port ${port}`);
|
|
185
244
|
});
|
|
186
245
|
} else {
|
|
246
|
+
const stdioServer = buildMcpServer();
|
|
187
247
|
const transport = new import_stdio.StdioServerTransport();
|
|
188
|
-
await
|
|
248
|
+
await stdioServer.connect(transport);
|
|
189
249
|
console.error("[ttt-mcp] OpenTTT MCP Server running on stdio");
|
|
190
250
|
}
|
|
191
251
|
}
|
package/dist/tools.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,24 +17,52 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var tools_exports = {};
|
|
20
30
|
__export(tools_exports, {
|
|
31
|
+
potCheckpoint: () => potCheckpoint,
|
|
21
32
|
potGenerate: () => potGenerate,
|
|
33
|
+
potGraph: () => potGraph,
|
|
22
34
|
potHealth: () => potHealth,
|
|
23
35
|
potQuery: () => potQuery,
|
|
24
36
|
potStats: () => potStats,
|
|
25
|
-
potVerify: () => potVerify
|
|
37
|
+
potVerify: () => potVerify,
|
|
38
|
+
redis: () => redis,
|
|
39
|
+
restoreDagEntry: () => restoreDagEntry
|
|
26
40
|
});
|
|
27
41
|
module.exports = __toCommonJS(tools_exports);
|
|
28
42
|
var import_openttt = require("openttt");
|
|
29
43
|
var import_telemetry = require("./telemetry");
|
|
44
|
+
var import_ioredis = __toESM(require("ioredis"));
|
|
45
|
+
const GrgPipeline = (
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
|
+
require("openttt").GrgPipeline ?? null
|
|
48
|
+
);
|
|
49
|
+
const redis = new import_ioredis.default(process.env.REDIS_URL ?? "redis://127.0.0.1:6379", {
|
|
50
|
+
lazyConnect: true,
|
|
51
|
+
enableOfflineQueue: false,
|
|
52
|
+
maxRetriesPerRequest: 1
|
|
53
|
+
});
|
|
54
|
+
redis.on("error", () => {
|
|
55
|
+
});
|
|
30
56
|
const timeSynth = new import_openttt.TimeSynthesis();
|
|
31
57
|
const adaptiveSwitch = new import_openttt.AdaptiveSwitch();
|
|
32
58
|
const potSigner = new import_openttt.PotSigner();
|
|
33
59
|
const startedAt = Date.now();
|
|
34
60
|
const POT_LOG_MAX = 1e4;
|
|
35
61
|
const potLog = [];
|
|
62
|
+
const potByEventId = /* @__PURE__ */ new Map();
|
|
63
|
+
const potByPrevEventId = /* @__PURE__ */ new Map();
|
|
64
|
+
const evictedEventIds = /* @__PURE__ */ new Set();
|
|
65
|
+
const EVICTED_MAX = 1e3;
|
|
36
66
|
function bigintReplacer(_key, value) {
|
|
37
67
|
return typeof value === "bigint" ? value.toString() : value;
|
|
38
68
|
}
|
|
@@ -40,26 +70,181 @@ function serialize(obj) {
|
|
|
40
70
|
return JSON.parse(JSON.stringify(obj, bigintReplacer));
|
|
41
71
|
}
|
|
42
72
|
const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/1744392/openttt-base-sepolia/v0.1.0";
|
|
73
|
+
function compressEntry(entry, depth, depthThreshold) {
|
|
74
|
+
const t = depthThreshold ?? { compact: 5, minimal: 20, rollup: 50 };
|
|
75
|
+
if (depth <= t.compact) return entry;
|
|
76
|
+
if (depth <= t.minimal) return {
|
|
77
|
+
// compact
|
|
78
|
+
eventId: entry.eventId,
|
|
79
|
+
potHash: entry.potHash,
|
|
80
|
+
timestamp: entry.timestamp,
|
|
81
|
+
prevEventId: entry.prevEventId
|
|
82
|
+
};
|
|
83
|
+
if (depth <= t.rollup) return {
|
|
84
|
+
// minimal
|
|
85
|
+
eventId: entry.eventId,
|
|
86
|
+
timestamp: entry.timestamp
|
|
87
|
+
};
|
|
88
|
+
return `${entry.eventId ?? "?"}@${entry.timestamp}`;
|
|
89
|
+
}
|
|
90
|
+
function depthThresholdFromTokens(maxTokens, entryCount) {
|
|
91
|
+
const charBudget = maxTokens * 4;
|
|
92
|
+
const fullBudget = charBudget * 0.6;
|
|
93
|
+
const compactBudget = charBudget * 0.25;
|
|
94
|
+
const minimalBudget = charBudget * 0.1;
|
|
95
|
+
const fullDepth = Math.max(1, Math.floor(fullBudget / 300));
|
|
96
|
+
const compactDepth = fullDepth + Math.max(0, Math.floor(compactBudget / 120));
|
|
97
|
+
const minimalDepth = compactDepth + Math.max(0, Math.floor(minimalBudget / 60));
|
|
98
|
+
return {
|
|
99
|
+
compact: Math.min(fullDepth, entryCount),
|
|
100
|
+
minimal: Math.min(compactDepth, entryCount),
|
|
101
|
+
rollup: Math.min(minimalDepth, entryCount)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function restoreDagEntry(entry) {
|
|
105
|
+
if (potByEventId.has(entry.eventId)) return;
|
|
106
|
+
const e = {
|
|
107
|
+
potHash: entry.potHash,
|
|
108
|
+
timestamp: entry.timestamp,
|
|
109
|
+
stratum: entry.stratum,
|
|
110
|
+
mode: entry.mode,
|
|
111
|
+
chainId: entry.chainId ?? void 0,
|
|
112
|
+
poolAddress: entry.poolAddress ?? void 0,
|
|
113
|
+
eventId: entry.eventId,
|
|
114
|
+
prevEventId: entry.prevEventId ?? void 0,
|
|
115
|
+
createdAt: entry.createdAt
|
|
116
|
+
};
|
|
117
|
+
if (potLog.length >= POT_LOG_MAX) {
|
|
118
|
+
const evicted = potLog.shift();
|
|
119
|
+
if (evicted.eventId) {
|
|
120
|
+
potByEventId.delete(evicted.eventId);
|
|
121
|
+
evictedEventIds.add(evicted.eventId);
|
|
122
|
+
if (evictedEventIds.size > EVICTED_MAX) {
|
|
123
|
+
const first = evictedEventIds.values().next().value;
|
|
124
|
+
if (first !== void 0) evictedEventIds.delete(first);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (evicted.prevEventId) {
|
|
128
|
+
const siblings = potByPrevEventId.get(evicted.prevEventId);
|
|
129
|
+
if (siblings) {
|
|
130
|
+
const filtered = siblings.filter((s) => s !== evicted);
|
|
131
|
+
if (filtered.length === 0) potByPrevEventId.delete(evicted.prevEventId);
|
|
132
|
+
else potByPrevEventId.set(evicted.prevEventId, filtered);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
potLog.push(e);
|
|
137
|
+
potByEventId.set(entry.eventId, e);
|
|
138
|
+
if (entry.prevEventId) {
|
|
139
|
+
const bucket = potByPrevEventId.get(entry.prevEventId) ?? [];
|
|
140
|
+
bucket.push(e);
|
|
141
|
+
potByPrevEventId.set(entry.prevEventId, bucket);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
43
144
|
async function potGenerate(args) {
|
|
145
|
+
if (!args.eventId && !args.txHash) {
|
|
146
|
+
throw new Error("Either eventId (Claude Code) or txHash (DeFi) is required");
|
|
147
|
+
}
|
|
44
148
|
(0, import_telemetry.telemetryIncrement)("pot_generate");
|
|
45
|
-
|
|
149
|
+
let pot;
|
|
150
|
+
let isOfflineFallback = false;
|
|
151
|
+
try {
|
|
152
|
+
pot = await timeSynth.generateProofOfTime();
|
|
153
|
+
} catch {
|
|
154
|
+
isOfflineFallback = true;
|
|
155
|
+
const nowMs = BigInt(Date.now());
|
|
156
|
+
pot = {
|
|
157
|
+
timestamp: nowMs * 1000000n,
|
|
158
|
+
stratum: 16,
|
|
159
|
+
uncertainty: 999999999,
|
|
160
|
+
confidence: 0,
|
|
161
|
+
sources: 0,
|
|
162
|
+
nonce: Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("hex"),
|
|
163
|
+
expiresAt: (nowMs + 300000n) * 1000000n,
|
|
164
|
+
sourceReadings: []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
46
167
|
const potHash = import_openttt.TimeSynthesis.getOnChainHash(pot);
|
|
47
|
-
|
|
48
|
-
|
|
168
|
+
let grgShards = [];
|
|
169
|
+
let currentMode;
|
|
170
|
+
if (args.txHash && args.chainId != null && args.poolAddress) {
|
|
171
|
+
const txData = new TextEncoder().encode(args.txHash);
|
|
172
|
+
grgShards = GrgPipeline.processForward(txData, args.chainId, args.poolAddress).map((s) => Buffer.from(s).toString("hex"));
|
|
173
|
+
const syntheticBlock = {
|
|
174
|
+
timestamp: Number(pot.timestamp / 1000000n),
|
|
175
|
+
// ns → ms
|
|
176
|
+
txs: [args.txHash],
|
|
177
|
+
data: new TextEncoder().encode(args.txHash)
|
|
178
|
+
};
|
|
179
|
+
const syntheticTTTRecord = {
|
|
180
|
+
time: Number(pot.timestamp / 1000000n),
|
|
181
|
+
txOrder: [args.txHash],
|
|
182
|
+
grgPayload: []
|
|
183
|
+
};
|
|
184
|
+
currentMode = adaptiveSwitch.verifyBlock(syntheticBlock, syntheticTTTRecord, args.chainId, args.poolAddress);
|
|
185
|
+
} else {
|
|
186
|
+
currentMode = adaptiveSwitch.getCurrentMode();
|
|
187
|
+
}
|
|
49
188
|
const signature = potSigner.signPot(potHash);
|
|
50
189
|
const entry = {
|
|
51
190
|
potHash,
|
|
52
191
|
timestamp: pot.timestamp.toString(),
|
|
53
192
|
stratum: pot.stratum,
|
|
54
|
-
mode:
|
|
193
|
+
mode: currentMode,
|
|
55
194
|
chainId: args.chainId,
|
|
56
195
|
poolAddress: args.poolAddress,
|
|
196
|
+
eventId: args.eventId,
|
|
197
|
+
prevEventId: args.prevEventId,
|
|
57
198
|
createdAt: Date.now()
|
|
58
199
|
};
|
|
200
|
+
if (potLog.length >= POT_LOG_MAX) {
|
|
201
|
+
const evicted = potLog.shift();
|
|
202
|
+
if (evicted.eventId) {
|
|
203
|
+
potByEventId.delete(evicted.eventId);
|
|
204
|
+
evictedEventIds.add(evicted.eventId);
|
|
205
|
+
if (evictedEventIds.size > EVICTED_MAX) {
|
|
206
|
+
const first = evictedEventIds.values().next().value;
|
|
207
|
+
if (first !== void 0) evictedEventIds.delete(first);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (evicted.prevEventId) {
|
|
211
|
+
const siblings = potByPrevEventId.get(evicted.prevEventId);
|
|
212
|
+
if (siblings) {
|
|
213
|
+
const filtered = siblings.filter((e) => e !== evicted);
|
|
214
|
+
if (filtered.length === 0) potByPrevEventId.delete(evicted.prevEventId);
|
|
215
|
+
else potByPrevEventId.set(evicted.prevEventId, filtered);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
59
219
|
potLog.push(entry);
|
|
60
|
-
if (
|
|
220
|
+
if (args.eventId) potByEventId.set(args.eventId, entry);
|
|
221
|
+
if (args.prevEventId) {
|
|
222
|
+
const bucket = potByPrevEventId.get(args.prevEventId) ?? [];
|
|
223
|
+
bucket.push(entry);
|
|
224
|
+
potByPrevEventId.set(args.prevEventId, bucket);
|
|
225
|
+
}
|
|
226
|
+
if (args.eventId) {
|
|
227
|
+
redis.setex(
|
|
228
|
+
`dag:${args.eventId}`,
|
|
229
|
+
90 * 86400,
|
|
230
|
+
JSON.stringify({
|
|
231
|
+
eventId: args.eventId,
|
|
232
|
+
prevEventId: args.prevEventId ?? null,
|
|
233
|
+
potHash,
|
|
234
|
+
timestamp: pot.timestamp.toString(),
|
|
235
|
+
stratum: pot.stratum,
|
|
236
|
+
mode: currentMode,
|
|
237
|
+
createdAt: entry.createdAt,
|
|
238
|
+
chainId: args.chainId ?? null,
|
|
239
|
+
poolAddress: args.poolAddress ?? null
|
|
240
|
+
})
|
|
241
|
+
).catch(() => {
|
|
242
|
+
});
|
|
243
|
+
}
|
|
61
244
|
return serialize({
|
|
62
245
|
potHash,
|
|
246
|
+
eventId: args.eventId ?? null,
|
|
247
|
+
prevEventId: args.prevEventId ?? null,
|
|
63
248
|
timestamp: pot.timestamp.toString(),
|
|
64
249
|
stratum: pot.stratum,
|
|
65
250
|
uncertainty: pot.uncertainty,
|
|
@@ -67,7 +252,8 @@ async function potGenerate(args) {
|
|
|
67
252
|
sources: pot.sources,
|
|
68
253
|
nonce: pot.nonce,
|
|
69
254
|
expiresAt: pot.expiresAt.toString(),
|
|
70
|
-
|
|
255
|
+
...isOfflineFallback && { mode: "local" },
|
|
256
|
+
...grgShards.length > 0 && { grgShards },
|
|
71
257
|
signature: {
|
|
72
258
|
issuerPubKey: signature.issuerPubKey,
|
|
73
259
|
signature: signature.signature,
|
|
@@ -81,7 +267,7 @@ async function potVerify(args) {
|
|
|
81
267
|
let valid = false;
|
|
82
268
|
let reconstructedSize = 0;
|
|
83
269
|
try {
|
|
84
|
-
const recovered =
|
|
270
|
+
const recovered = GrgPipeline.processInverse(shards, 0, args.chainId, args.poolAddress);
|
|
85
271
|
valid = recovered.length > 0;
|
|
86
272
|
reconstructedSize = recovered.length;
|
|
87
273
|
} catch {
|
|
@@ -98,6 +284,16 @@ async function potVerify(args) {
|
|
|
98
284
|
}
|
|
99
285
|
async function potQuery(args) {
|
|
100
286
|
(0, import_telemetry.telemetryIncrement)("pot_query");
|
|
287
|
+
if (args.eventId) {
|
|
288
|
+
const entry = potByEventId.get(args.eventId);
|
|
289
|
+
return serialize({
|
|
290
|
+
local: entry ? [entry] : [],
|
|
291
|
+
subgraph: [],
|
|
292
|
+
found: !!entry,
|
|
293
|
+
totalLocal: potLog.length,
|
|
294
|
+
query: { eventId: args.eventId }
|
|
295
|
+
});
|
|
296
|
+
}
|
|
101
297
|
const limit = Math.min(args.limit ?? 100, 1e3);
|
|
102
298
|
const now = Date.now();
|
|
103
299
|
const startTime = args.startTime ?? now - 864e5;
|
|
@@ -140,6 +336,35 @@ async function potQuery(args) {
|
|
|
140
336
|
query: { startTime, endTime, limit }
|
|
141
337
|
});
|
|
142
338
|
}
|
|
339
|
+
async function potGraph(args) {
|
|
340
|
+
(0, import_telemetry.telemetryIncrement)("pot_graph");
|
|
341
|
+
const maxDepth = Math.min(args.depth ?? 10, 100);
|
|
342
|
+
const backwardChain = [];
|
|
343
|
+
let cursor = potByEventId.get(args.eventId);
|
|
344
|
+
let d = 0;
|
|
345
|
+
while (cursor && d < maxDepth) {
|
|
346
|
+
backwardChain.unshift(cursor);
|
|
347
|
+
cursor = cursor.prevEventId ? potByEventId.get(cursor.prevEventId) : void 0;
|
|
348
|
+
d++;
|
|
349
|
+
}
|
|
350
|
+
const forwardChain = potByPrevEventId.get(args.eventId) ?? [];
|
|
351
|
+
const found = potByEventId.has(args.eventId);
|
|
352
|
+
const chainBroken = backwardChain.some((e) => e.eventId && evictedEventIds.has(e.eventId)) || cursor?.prevEventId != null && potByEventId.get(cursor.prevEventId) == null;
|
|
353
|
+
const chainRoot = backwardChain.length > 0 ? backwardChain[0] : null;
|
|
354
|
+
const isServerRestart = chainBroken && chainRoot !== null && chainRoot.prevEventId != null && !potByEventId.has(chainRoot.prevEventId) && !evictedEventIds.has(chainRoot.prevEventId);
|
|
355
|
+
const brokenAt = chainBroken ? isServerRestart ? "server_restart" : backwardChain[0]?.eventId ?? null : null;
|
|
356
|
+
const compressedBackward = backwardChain.map((e, i) => compressEntry(e, i + 1));
|
|
357
|
+
return serialize({
|
|
358
|
+
eventId: args.eventId,
|
|
359
|
+
found,
|
|
360
|
+
backwardChain: compressedBackward,
|
|
361
|
+
forwardChain,
|
|
362
|
+
chainLength: backwardChain.length + forwardChain.length,
|
|
363
|
+
reachableDepth: backwardChain.length,
|
|
364
|
+
chainBroken,
|
|
365
|
+
brokenAt
|
|
366
|
+
});
|
|
367
|
+
}
|
|
143
368
|
async function potStats(args) {
|
|
144
369
|
(0, import_telemetry.telemetryIncrement)("pot_stats");
|
|
145
370
|
const now = Date.now();
|
|
@@ -220,11 +445,54 @@ async function potHealth() {
|
|
|
220
445
|
}
|
|
221
446
|
});
|
|
222
447
|
}
|
|
448
|
+
async function potCheckpoint(args) {
|
|
449
|
+
(0, import_telemetry.telemetryIncrement)("pot_checkpoint");
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
const startTime = args.startTime ?? now - 36e5;
|
|
452
|
+
const endTime = args.endTime ?? now;
|
|
453
|
+
let entries;
|
|
454
|
+
if (args.fromEventId && args.toEventId) {
|
|
455
|
+
entries = [];
|
|
456
|
+
let cursor = potByEventId.get(args.fromEventId);
|
|
457
|
+
const maxDepth = 1e3;
|
|
458
|
+
let d = 0;
|
|
459
|
+
while (cursor && d < maxDepth) {
|
|
460
|
+
entries.push(cursor);
|
|
461
|
+
if (cursor.eventId === args.toEventId) break;
|
|
462
|
+
const nexts = potByPrevEventId.get(cursor.eventId ?? "") ?? [];
|
|
463
|
+
cursor = nexts[0];
|
|
464
|
+
d++;
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
entries = potLog.filter((e) => e.createdAt >= startTime && e.createdAt <= endTime);
|
|
468
|
+
}
|
|
469
|
+
const eventCount = entries.length;
|
|
470
|
+
const depthThreshold = args.maxTokens ? depthThresholdFromTokens(args.maxTokens, entries.length) : void 0;
|
|
471
|
+
const compressed = entries.map((e, i) => compressEntry(e, i + 1, depthThreshold));
|
|
472
|
+
const chainIntact = !entries.some((e) => e.eventId && evictedEventIds.has(e.eventId));
|
|
473
|
+
const nextCheckpointHint = Math.max(10, 100 - eventCount % 100);
|
|
474
|
+
const checkpointId = `ckpt_${now}_${eventCount}`;
|
|
475
|
+
const firstTs = entries[0]?.timestamp ?? null;
|
|
476
|
+
const lastTs = entries[entries.length - 1]?.timestamp ?? null;
|
|
477
|
+
return serialize({
|
|
478
|
+
checkpointId,
|
|
479
|
+
eventCount,
|
|
480
|
+
chainIntact,
|
|
481
|
+
nextCheckpointHint,
|
|
482
|
+
rollup: compressed,
|
|
483
|
+
summary: `${eventCount} events from ${firstTs ?? "?"} to ${lastTs ?? "?"}`,
|
|
484
|
+
generatedAt: now
|
|
485
|
+
});
|
|
486
|
+
}
|
|
223
487
|
// Annotate the CommonJS export names for ESM import in node:
|
|
224
488
|
0 && (module.exports = {
|
|
489
|
+
potCheckpoint,
|
|
225
490
|
potGenerate,
|
|
491
|
+
potGraph,
|
|
226
492
|
potHealth,
|
|
227
493
|
potQuery,
|
|
228
494
|
potStats,
|
|
229
|
-
potVerify
|
|
495
|
+
potVerify,
|
|
496
|
+
redis,
|
|
497
|
+
restoreDagEntry
|
|
230
498
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helm-protocol/ttt-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP Server for OpenTTT — Proof of Time tools for AI agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,23 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "esbuild index.ts tools.ts telemetry.ts auth.ts --platform=node --target=node18 --format=cjs --outdir=dist",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
|
-
"dev": "npx ts-node index.ts"
|
|
16
|
+
"dev": "npx ts-node index.ts",
|
|
17
|
+
"test": "jest --forceExit"
|
|
18
|
+
},
|
|
19
|
+
"jest": {
|
|
20
|
+
"preset": "ts-jest",
|
|
21
|
+
"testEnvironment": "node",
|
|
22
|
+
"testMatch": ["**/__tests__/**/*.test.ts"],
|
|
23
|
+
"moduleNameMapper": {
|
|
24
|
+
"^openttt$": "<rootDir>/node_modules/openttt/dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"transform": {
|
|
27
|
+
"^.+\\.tsx?$": ["ts-jest", {
|
|
28
|
+
"tsconfig": {
|
|
29
|
+
"module": "CommonJS"
|
|
30
|
+
}
|
|
31
|
+
}]
|
|
32
|
+
}
|
|
17
33
|
},
|
|
18
34
|
"keywords": [
|
|
19
35
|
"mcp",
|
|
@@ -36,12 +52,16 @@
|
|
|
36
52
|
},
|
|
37
53
|
"dependencies": {
|
|
38
54
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
39
|
-
"
|
|
55
|
+
"ioredis": "^5.11.0",
|
|
56
|
+
"openttt": "^0.2.13",
|
|
40
57
|
"zod": "^3.25.0"
|
|
41
58
|
},
|
|
42
59
|
"devDependencies": {
|
|
60
|
+
"@types/jest": "^30.0.0",
|
|
43
61
|
"@types/node": "^20.11.19",
|
|
44
62
|
"esbuild": "^0.27.4",
|
|
63
|
+
"jest": "^30.4.2",
|
|
64
|
+
"ts-jest": "^29.4.11",
|
|
45
65
|
"typescript": "^5.3.3"
|
|
46
66
|
},
|
|
47
67
|
"mcpName": "io.github.Helm-Protocol/openttt-pot"
|