@rel-time/mcp-server 0.0.0-stage → 1.2.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.
Files changed (3) hide show
  1. package/README.md +45 -2
  2. package/index.js +846 -0
  3. package/package.json +35 -4
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # `@rel-time/mcp-server` — Official Rel-Time MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official Model Context Protocol (MCP) server for **[Rel-Time](https://rel-time.com)** — enabling Claude Desktop, Cursor, Windsurf, and Google Antigravity agents to:
4
+ 1. **Hash local files with 100% Zero-Knowledge** (your file bytes never leave your computer).
5
+ 2. **Execute both Anonymous & Biometric (Passkey) Anchors** (`SHA256(File_Hash + Passkey_PubKey)`) using TouchID, FaceID, or YubiKey via `https://rel-time.com/mcp-passkey`.
6
+ 3. **Anchor SHA-256 hashes to the Bitcoin Timechain & Rel-Time VDF** (`POST /v1/anchor`).
7
+ 4. **Generate & overlay 64-ray Sun-Stamps onto PDFs and Images** (`reltime-stamped-<name>.pdf` / `.png`).
8
+ 5. **Verify Bitcoin block settlement & Merkle proofs** (`GET /v1/proof/{hash}`).
9
+ 6. **Self-provision or Reload (+500 Hash Credits / $50) an Autonomous Agent API key via Lightning** (`POST /v1/agent/invoice` & `POST /v1/agent/claim`).
10
+
11
+ ---
12
+
13
+ ## Quick Setup (Claude Desktop / Cursor / Windsurf / Antigravity)
14
+
15
+ Add the following to your `claude_desktop_config.json` or `.cursor/mcp.json`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "rel-time": {
21
+ "command": "npx",
22
+ "args": ["-y", "@rel-time/mcp-server"],
23
+ "env": {
24
+ "RELTIME_API_KEY": "YOUR_RELTIME_API_KEY"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ *(Note: `reltime_link_passkey`, `reltime_get_sun_stamp`, `reltime_check_proof`, and `reltime_provision_agent_key` work immediately even without an API key configured!)*
32
+
33
+ ---
34
+
35
+ ## Available MCP Tools
36
+
37
+ | Tool Name | Requires Key? | What It Does |
38
+ | :--- | :---: | :--- |
39
+ | `reltime_anchor_file` | Yes | Computes the SHA-256 hash of a local file **on your machine** (0 bytes uploaded), supports `biometric: true` for **Biometric Anchors** (TouchID/FaceID/YubiKey), anchors via `POST /v1/anchor`, saves `reltime-stamp-<filename>.svg` and `reltime-doomsday-proof-<filename>.json`, and automatically outputs a **Stamped Copy** (`reltime-stamped-<name>.pdf` or `.png`). |
40
+ | `reltime_link_passkey` | **No** | Opens `https://rel-time.com/mcp-passkey` in the system browser so the user can touch TouchID / FaceID / YubiKey once, saving `{ public_key_hex, passkey_signature }` to `~/.reltime/passkey.json` for seamless Biometric Anchors. |
41
+ | `reltime_stamp_file` | **No** | Overlays the authentic 64-ray Sun-Stamp onto the bottom-right corner of any local PDF or Image (`.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`) 100% locally. |
42
+ | `reltime_anchor_hash` | Yes | Anchors any 64-char hex SHA-256 hash in `standard` (10-min Express / Integrity) or `temporal` (VDF sequence ordering) mode. |
43
+ | `reltime_get_sun_stamp` | **No** | Fetches the deterministic 64-ray SVG Sun-Stamp (`GET /v1/stamp/{hash}`) and returns ready-to-paste Markdown/HTML badge snippets. |
44
+ | `reltime_check_proof` | **No** | Queries `GET /v1/proof/{hash}` for Bitcoin TXID, Block Height, UTC Timestamp, and Merkle inclusion path, and optionally saves/updates the `.json` Doomsday Proof receipt on disk. |
45
+ | `reltime_account_status` | Yes | Checks remaining Hash Credits and tier status (`GET /v1/account`). |
46
+ | `reltime_provision_agent_key` | **No** | Generates a Lightning invoice (`$50` for 500 Hash Credits) and claims either a brand-new Tier 4 Autonomous Agent API key or **reloads `+500` Hash Credits into an existing key** (`reload_existing: true` or `api_key`). |
package/index.js ADDED
@@ -0,0 +1,846 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @rel-time/mcp-server
5
+ * Official Model Context Protocol (MCP) Server for Rel-Time (https://rel-time.com)
6
+ *
7
+ * Zero-Knowledge Local File Hashing + Biometric Anchor (Passkey) +
8
+ * Bitcoin Timechain Anchoring + Deterministic Sun-Stamp SVG +
9
+ * 1-Click Option A Stamped Copy (.PDF / .PNG).
10
+ * Uses native Node.js (>=18) crypto, fs, http, and fetch over JSON-RPC 2.0 stdio transport.
11
+ */
12
+
13
+ const crypto = require("crypto");
14
+ const fs = require("fs");
15
+ const http = require("http");
16
+ const os = require("os");
17
+ const path = require("path");
18
+ const readline = require("readline");
19
+ const { exec } = require("child_process");
20
+ const { PDFDocument } = require("pdf-lib");
21
+ const sharp = require("sharp");
22
+
23
+ const API_BASE = (process.env.RELTIME_API_URL || "https://api.rel-time.com/v1").replace(/\/+$/, "");
24
+ const WEB_BASE = (process.env.RELTIME_WEB_URL || "https://rel-time.com").replace(/\/+$/, "");
25
+ let CURRENT_API_KEY = process.env.RELTIME_API_KEY || "";
26
+
27
+ const PASSKEY_CONFIG_DIR = path.join(os.homedir(), ".reltime");
28
+ const PASSKEY_CONFIG_PATH = path.join(PASSKEY_CONFIG_DIR, "passkey.json");
29
+
30
+ const SERVER_INFO = {
31
+ name: "rel-time-mcp-server",
32
+ version: "1.2.0",
33
+ };
34
+
35
+ /**
36
+ * Loads linked Passkey credentials from env vars or ~/.reltime/passkey.json.
37
+ */
38
+ function loadSavedPasskey() {
39
+ if (process.env.RELTIME_PASSKEY_PUBKEY && process.env.RELTIME_PASSKEY_TOKEN) {
40
+ return {
41
+ public_key_hex: process.env.RELTIME_PASSKEY_PUBKEY.trim(),
42
+ passkey_signature: process.env.RELTIME_PASSKEY_TOKEN.trim(),
43
+ source: "env",
44
+ };
45
+ }
46
+ try {
47
+ if (fs.existsSync(PASSKEY_CONFIG_PATH)) {
48
+ const parsed = JSON.parse(fs.readFileSync(PASSKEY_CONFIG_PATH, "utf8"));
49
+ if (parsed.public_key_hex && parsed.passkey_signature) {
50
+ return {
51
+ public_key_hex: parsed.public_key_hex.trim(),
52
+ passkey_signature: parsed.passkey_signature.trim(),
53
+ source: PASSKEY_CONFIG_PATH,
54
+ };
55
+ }
56
+ }
57
+ } catch (e) {
58
+ // Ignore corrupted file
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Saves Passkey credentials to ~/.reltime/passkey.json with 0600 permissions.
65
+ */
66
+ function savePasskeyConfig(publicKeyHex, passkeySignature) {
67
+ if (!fs.existsSync(PASSKEY_CONFIG_DIR)) {
68
+ fs.mkdirSync(PASSKEY_CONFIG_DIR, { recursive: true, mode: 0o700 });
69
+ }
70
+ const payload = {
71
+ public_key_hex: publicKeyHex.trim(),
72
+ passkey_signature: passkeySignature.trim(),
73
+ linked_at: new Date().toISOString(),
74
+ };
75
+ fs.writeFileSync(PASSKEY_CONFIG_PATH, JSON.stringify(payload, null, 2), {
76
+ encoding: "utf8",
77
+ mode: 0o600,
78
+ });
79
+ return payload;
80
+ }
81
+
82
+ /**
83
+ * Opens a URL in the user's default system browser.
84
+ */
85
+ function openSystemBrowser(url) {
86
+ const platform = process.platform;
87
+ const cmd =
88
+ platform === "darwin"
89
+ ? `open "${url}"`
90
+ : platform === "win32"
91
+ ? `start "" "${url}"`
92
+ : `xdg-open "${url}"`;
93
+ exec(cmd);
94
+ }
95
+
96
+ /**
97
+ * Launches a temporary 127.0.0.1 HTTP callback listener and opens the
98
+ * Rel-Time WebAuthn Passkey Bridge in the user's browser so they can touch
99
+ * TouchID / FaceID / YubiKey.
100
+ */
101
+ function launchBrowserPasskeyBridge(timeoutMs = 120000) {
102
+ return new Promise((resolve, reject) => {
103
+ const server = http.createServer((req, res) => {
104
+ res.setHeader("Access-Control-Allow-Origin", "*");
105
+ res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
106
+ if (req.method === "OPTIONS") {
107
+ res.writeHead(204);
108
+ res.end();
109
+ return;
110
+ }
111
+
112
+ const parsedUrl = new URL(req.url, "http://127.0.0.1");
113
+ if (parsedUrl.pathname === "/callback") {
114
+ const pubKey = parsedUrl.searchParams.get("public_key_hex") || parsedUrl.searchParams.get("public_key");
115
+ const token = parsedUrl.searchParams.get("passkey_signature");
116
+ if (pubKey && token) {
117
+ const saved = savePasskeyConfig(pubKey, token);
118
+ res.writeHead(200, { "Content-Type": "application/json" });
119
+ res.end(JSON.stringify({ status: "linked" }));
120
+ clearTimeout(timer);
121
+ server.close();
122
+ resolve(saved);
123
+ return;
124
+ }
125
+ }
126
+
127
+ res.writeHead(400, { "Content-Type": "application/json" });
128
+ res.end(JSON.stringify({ error: "Missing public_key_hex or passkey_signature" }));
129
+ });
130
+
131
+ const timer = setTimeout(() => {
132
+ server.close();
133
+ reject(
134
+ new Error(
135
+ `Timed out waiting for hardware Passkey touch in browser. Please complete the prompt at ${WEB_BASE}/mcp-passkey or pass public_key_hex and passkey_signature directly to reltime_link_passkey.`
136
+ )
137
+ );
138
+ }, timeoutMs);
139
+
140
+ server.listen(0, "127.0.0.1", () => {
141
+ const address = server.address();
142
+ const port = address.port;
143
+ const bridgeUrl = `${WEB_BASE}/mcp-passkey?port=${port}`;
144
+ openSystemBrowser(bridgeUrl);
145
+ });
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Stamps the Option A (pure crisp 64-ray Sun-Stamp, 0% glare, no pill) onto a local Image or PDF.
151
+ * Returns { stamped: true, output_path, format } or { stamped: false, reason }.
152
+ */
153
+ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = null) {
154
+ const ext = path.extname(filePath).toLowerCase().replace(/^\./, "");
155
+ const dir = path.dirname(filePath);
156
+ const baseName = path.basename(filePath, path.extname(filePath));
157
+
158
+ const isImage = ["png", "jpg", "jpeg", "webp", "gif"].includes(ext);
159
+ const isPdf = ext === "pdf";
160
+
161
+ if (!isImage && !isPdf) {
162
+ return {
163
+ stamped: false,
164
+ reason: "Feature unavailable for this file type (visual Sun-Stamp overlay is only available for Images and PDFs).",
165
+ };
166
+ }
167
+
168
+ if (isImage) {
169
+ const outPath = customOutputPath ? path.resolve(customOutputPath) : path.join(dir, `reltime-stamped-${baseName}.png`);
170
+ const img = sharp(filePath);
171
+ const meta = await img.metadata();
172
+ const w = meta.width || 1200;
173
+ const h = meta.height || 800;
174
+
175
+ const minDim = Math.min(w, h);
176
+ const badgeSize = Math.max(130, Math.min(420, Math.round(minDim * 0.22)));
177
+ const margin = Math.max(12, Math.round(minDim * 0.025));
178
+
179
+ const stampPngBuffer = await sharp(Buffer.from(svgContent, "utf8"))
180
+ .resize(badgeSize, badgeSize)
181
+ .png()
182
+ .toBuffer();
183
+
184
+ const left = Math.max(0, w - badgeSize - margin);
185
+ const top = Math.max(0, h - badgeSize - margin);
186
+
187
+ await img
188
+ .composite([{ input: stampPngBuffer, left, top }])
189
+ .png()
190
+ .toFile(outPath);
191
+
192
+ return {
193
+ stamped: true,
194
+ format: "PNG",
195
+ output_path: outPath,
196
+ };
197
+ }
198
+
199
+ if (isPdf) {
200
+ const outPath = customOutputPath ? path.resolve(customOutputPath) : path.join(dir, `reltime-stamped-${baseName}.pdf`);
201
+ const pdfBytes = fs.readFileSync(filePath);
202
+ const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
203
+ const pages = pdfDoc.getPages();
204
+ if (pages.length === 0) {
205
+ throw new Error("PDF has no pages.");
206
+ }
207
+
208
+ const firstPage = pages[0];
209
+ const { width, height } = firstPage.getSize();
210
+
211
+ // Render high-DPI 480x480 Option A Sun-Stamp PNG (pure crisp SVG, 0% glare, no pill)
212
+ const stampPngBuffer = await sharp(Buffer.from(svgContent, "utf8"))
213
+ .resize(480, 480)
214
+ .png()
215
+ .toBuffer();
216
+
217
+ const pngImage = await pdfDoc.embedPng(stampPngBuffer);
218
+ const targetSize = Math.min(135, Math.max(85, Math.min(width, height) * 0.2));
219
+ const margin = 18;
220
+
221
+ firstPage.drawImage(pngImage, {
222
+ x: Math.max(margin, width - targetSize - margin),
223
+ y: margin,
224
+ width: targetSize,
225
+ height: targetSize,
226
+ });
227
+
228
+ const modifiedPdfBytes = await pdfDoc.save();
229
+ fs.writeFileSync(outPath, Buffer.from(modifiedPdfBytes));
230
+
231
+ return {
232
+ stamped: true,
233
+ format: "PDF",
234
+ output_path: outPath,
235
+ };
236
+ }
237
+ }
238
+
239
+ const TOOLS = [
240
+ {
241
+ name: "reltime_anchor_file",
242
+ description:
243
+ "Zero-knowledge file anchoring + 1-Click Stamped Copy: Computes the SHA-256 hash of a local file ENTIRELY ON THE LOCAL MACHINE (the file never leaves the user's computer), supports both Anonymous and Biometric (Passkey) Anchors, anchors the 64-char hash to the Bitcoin Timechain via Rel-Time, saves the 64-ray Sun-Stamp SVG and JSON Doomsday Proof, and automatically creates a Stamped Copy (`reltime-stamped-<name>.pdf` or `.png`) with the Sun-Stamp overlaid in the bottom-right corner for PDFs and Images.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ file_path: {
248
+ type: "string",
249
+ description: "Absolute path to the local file to hash and anchor.",
250
+ },
251
+ biometric: {
252
+ type: "boolean",
253
+ description:
254
+ "If true, performs a Biometric Anchor using your linked hardware Passkey (TouchID / FaceID / YubiKey) so the anchored hash (`SHA256(File_Hash + Passkey_PubKey)`) and Sun-Stamp mathematically prove your personal authorship. If no Passkey is linked yet, automatically opens the browser TouchID/FaceID prompt.",
255
+ },
256
+ anchor_mode: {
257
+ type: "string",
258
+ enum: ["standard", "temporal"],
259
+ description:
260
+ "Use 'standard' (default) to anchor the file/biometric SHA-256 hash into the Bitcoin Merkle tree for standalone file verification, or 'temporal' to anchor the VDF Thermodynamic Proof (T_index) for sub-second sequential ordering.",
261
+ },
262
+ save_sun_stamp_svg: {
263
+ type: "boolean",
264
+ description: "If true (default), saves `reltime-stamp-<filename>.svg` in the same folder as the file.",
265
+ },
266
+ save_stamped_copy: {
267
+ type: "boolean",
268
+ description:
269
+ "If true (default), automatically generates `reltime-stamped-<name>.pdf` or `.png` with the crisp 64-ray Sun-Stamp in the bottom-right corner when the file is a PDF or Image.",
270
+ },
271
+ api_key: {
272
+ type: "string",
273
+ description: "Optional override for RELTIME_API_KEY (Autonomous Agent, Universal Anchor, or Enterprise Superhighway key).",
274
+ },
275
+ },
276
+ required: ["file_path"],
277
+ },
278
+ },
279
+ {
280
+ name: "reltime_link_passkey",
281
+ description:
282
+ "Links the user's hardware Passkey (TouchID, FaceID, or YubiKey) to `@rel-time/mcp-server` for Biometric Anchors. Opens `https://rel-time.com/mcp-passkey` in the system browser, waits for the user's biometric touch, and saves `{ public_key_hex, passkey_signature }` to `~/.reltime/passkey.json`.",
283
+ inputSchema: {
284
+ type: "object",
285
+ properties: {
286
+ public_key_hex: {
287
+ type: "string",
288
+ description: "Optional: Directly provide the COSE public_key_hex from https://rel-time.com/mcp-passkey without opening a browser window.",
289
+ },
290
+ passkey_signature: {
291
+ type: "string",
292
+ description: "Optional: Directly provide the passkey_signature session token from https://rel-time.com/mcp-passkey.",
293
+ },
294
+ },
295
+ },
296
+ },
297
+ {
298
+ name: "reltime_stamp_file",
299
+ description:
300
+ "Overlays the authentic 64-ray Rel-Time Sun-Stamp (Option A: pure crisp Sun-Stamp, zero glare, no pill) onto the bottom-right corner of a local PDF or Image (.png, .jpg, .jpeg, .webp, .gif) 100% locally. Returns 'Feature unavailable for this file type' for other file formats.",
301
+ inputSchema: {
302
+ type: "object",
303
+ properties: {
304
+ file_path: {
305
+ type: "string",
306
+ description: "Absolute path to the local PDF or Image file.",
307
+ },
308
+ hash: {
309
+ type: "string",
310
+ description:
311
+ "Optional 64-char hex hash to generate the Sun-Stamp from. If omitted, computes the SHA-256 hash of `file_path` locally.",
312
+ },
313
+ output_path: {
314
+ type: "string",
315
+ description: "Optional custom output file path for the stamped copy.",
316
+ },
317
+ },
318
+ required: ["file_path"],
319
+ },
320
+ },
321
+ {
322
+ name: "reltime_anchor_hash",
323
+ description:
324
+ "Anchors a 64-character hexadecimal SHA-256 hash directly into the Rel-Time VDF & Bitcoin Merkle Tree. Consumes 1 Hash Credit from the configured Rel-Time API key.",
325
+ inputSchema: {
326
+ type: "object",
327
+ properties: {
328
+ client_hash: {
329
+ type: "string",
330
+ description: "64-character hex SHA-256 hash of the payload/artifact.",
331
+ },
332
+ anchor_mode: {
333
+ type: "string",
334
+ enum: ["standard", "temporal"],
335
+ description: "'standard' (Data Integrity) or 'temporal' (VDF Sequence Ordering).",
336
+ },
337
+ api_key: {
338
+ type: "string",
339
+ description: "Optional override for RELTIME_API_KEY.",
340
+ },
341
+ },
342
+ required: ["client_hash"],
343
+ },
344
+ },
345
+ {
346
+ name: "reltime_get_sun_stamp",
347
+ description:
348
+ "Generates and downloads the official 64-ray Rel-Time Sun-Stamp SVG (`GET /v1/stamp/{hash}`) for any 64-char SHA-256 hash or T_index. Requires NO API key and works across all tiers.",
349
+ inputSchema: {
350
+ type: "object",
351
+ properties: {
352
+ hash: {
353
+ type: "string",
354
+ description: "64-character hex SHA-256 hash (client_hash or t_index).",
355
+ },
356
+ output_svg_path: {
357
+ type: "string",
358
+ description: "Optional local file path where the `.svg` Sun-Stamp should be saved.",
359
+ },
360
+ },
361
+ required: ["hash"],
362
+ },
363
+ },
364
+ {
365
+ name: "reltime_check_proof",
366
+ description:
367
+ "Queries `GET /v1/proof/{hash}` to check the Bitcoin settlement status (Block Height, Block UTC Time, Bitcoin TXID, Merkle Root, and Merkle Inclusion Path) of an anchored hash. Optionally saves or updates the finalized `.json` Doomsday Proof receipt on disk when settled.",
368
+ inputSchema: {
369
+ type: "object",
370
+ properties: {
371
+ hash: {
372
+ type: "string",
373
+ description: "The 64-character client_hash or t_index to verify.",
374
+ },
375
+ output_json_path: {
376
+ type: "string",
377
+ description: "Optional local file path to save the settled JSON Doomsday Proof receipt.",
378
+ },
379
+ },
380
+ required: ["hash"],
381
+ },
382
+ },
383
+ {
384
+ name: "reltime_account_status",
385
+ description:
386
+ "Checks the remaining Hash Credits, tier, and lifetime usage of the configured Rel-Time API key (`GET /v1/account`).",
387
+ inputSchema: {
388
+ type: "object",
389
+ properties: {
390
+ api_key: {
391
+ type: "string",
392
+ description: "Optional override for RELTIME_API_KEY.",
393
+ },
394
+ },
395
+ },
396
+ },
397
+ {
398
+ name: "reltime_provision_agent_key",
399
+ description:
400
+ "Self-onboarding & Top-Up for Autonomous AI Agents: Requests a Bitcoin Lightning invoice (`POST /v1/agent/invoice`, $5 for 50 Hash Credits Launch Pack or $50 for 500 Hash Credits) or claims a paid invoice (`POST /v1/agent/claim`) to either provision a brand-new Tier 4 Autonomous Agent API key OR reload an existing key without changing the key.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ action: {
405
+ type: "string",
406
+ enum: ["create_invoice", "claim_invoice"],
407
+ description:
408
+ "'create_invoice' generates a BOLT11 Lightning invoice; 'claim_invoice' exchanges a paid invoice_id for a new API key or reloads an existing API key.",
409
+ },
410
+ pack: {
411
+ type: "string",
412
+ enum: ["launch", "standard"],
413
+ description:
414
+ "Optional: 'launch' ($5 for 50 Hash Credits) or 'standard' (default, $50 for 500 Hash Credits).",
415
+ },
416
+ invoice_id: {
417
+ type: "string",
418
+ description: "Required when action is 'claim_invoice'.",
419
+ },
420
+ api_key: {
421
+ type: "string",
422
+ description:
423
+ "Optional when action is 'claim_invoice': pass an existing `rel_key_agent_...` key (or omit to use RELTIME_API_KEY if reload_existing is true) to reload Hash Credits into that key without rotating it.",
424
+ },
425
+ reload_existing: {
426
+ type: "boolean",
427
+ description:
428
+ "If true during 'claim_invoice', reloads Hash Credits into the currently configured RELTIME_API_KEY (or passed `api_key`) instead of generating a new key.",
429
+ },
430
+ },
431
+ required: ["action"],
432
+ },
433
+ },
434
+ ];
435
+
436
+ async function executeTool(name, args = {}) {
437
+ const key = (args.api_key || CURRENT_API_KEY || "").trim();
438
+
439
+ if (name === "reltime_link_passkey") {
440
+ if (args.public_key_hex && args.passkey_signature) {
441
+ const saved = savePasskeyConfig(args.public_key_hex, args.passkey_signature);
442
+ return {
443
+ status: "linked",
444
+ mode: "direct",
445
+ config_path: PASSKEY_CONFIG_PATH,
446
+ public_key_hex: saved.public_key_hex,
447
+ linked_at: saved.linked_at,
448
+ message: "Hardware Passkey linked! You can now call reltime_anchor_file with biometric: true.",
449
+ };
450
+ }
451
+
452
+ const saved = await launchBrowserPasskeyBridge();
453
+ return {
454
+ status: "linked",
455
+ mode: "browser_webauthn_bridge",
456
+ config_path: PASSKEY_CONFIG_PATH,
457
+ public_key_hex: saved.public_key_hex,
458
+ linked_at: saved.linked_at,
459
+ message: "Hardware Passkey verified via TouchID/FaceID and saved to ~/.reltime/passkey.json! You can now call reltime_anchor_file with biometric: true.",
460
+ };
461
+ }
462
+
463
+ if (name === "reltime_anchor_file") {
464
+ const filePath = path.resolve(args.file_path);
465
+ if (!fs.existsSync(filePath)) {
466
+ throw new Error(`File not found at path: ${filePath}`);
467
+ }
468
+ if (!key) {
469
+ throw new Error(
470
+ "Missing RELTIME_API_KEY. Set RELTIME_API_KEY in your MCP config, pass `api_key`, or call `reltime_provision_agent_key` to get an Autonomous Agent key."
471
+ );
472
+ }
473
+
474
+ // 1. Compute SHA-256 locally (Zero-Knowledge: raw file never leaves the device)
475
+ const fileBuffer = fs.readFileSync(filePath);
476
+ const originalFileHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
477
+ const anchorMode = args.anchor_mode || "standard";
478
+
479
+ let clientHash = originalFileHash;
480
+ let passkeyCreds = null;
481
+
482
+ if (args.biometric) {
483
+ passkeyCreds = loadSavedPasskey();
484
+ if (!passkeyCreds) {
485
+ // Automatically launch the browser TouchID / FaceID bridge if not linked yet
486
+ passkeyCreds = await launchBrowserPasskeyBridge();
487
+ }
488
+ clientHash = crypto
489
+ .createHash("sha256")
490
+ .update(`${originalFileHash}${passkeyCreds.public_key_hex}`, "utf8")
491
+ .digest("hex");
492
+ }
493
+
494
+ const requestBody = {
495
+ client_hash: clientHash,
496
+ anchor_mode: anchorMode,
497
+ };
498
+
499
+ if (args.biometric && passkeyCreds) {
500
+ requestBody.original_file_hash = originalFileHash;
501
+ requestBody.passkey_signature = passkeyCreds.passkey_signature;
502
+ }
503
+
504
+ // 2. Anchor the hash via POST /v1/anchor
505
+ const res = await fetch(`${API_BASE}/anchor`, {
506
+ method: "POST",
507
+ headers: {
508
+ "Content-Type": "application/json",
509
+ Authorization: `Bearer ${key}`,
510
+ },
511
+ body: JSON.stringify(requestBody),
512
+ });
513
+
514
+ const data = await res.json();
515
+ if (!res.ok) {
516
+ throw new Error(`Rel-Time API error (${res.status}): ${JSON.stringify(data)}`);
517
+ }
518
+
519
+ const tIndex = data.t_index || data.thermodynamic_proof;
520
+ const lookupHash = anchorMode === "temporal" ? tIndex : clientHash;
521
+ const verifyUrl = `https://rel-time.com/verify?t=${lookupHash}`;
522
+ const stampEndpointUrl = `${API_BASE}/stamp/${lookupHash}`;
523
+
524
+ const svgContent = data.visual_fingerprint || (await (await fetch(stampEndpointUrl)).text());
525
+
526
+ let savedSvgPath = null;
527
+ if (args.save_sun_stamp_svg !== false) {
528
+ savedSvgPath = path.join(path.dirname(filePath), `reltime-stamp-${path.basename(filePath)}.svg`);
529
+ fs.writeFileSync(savedSvgPath, svgContent, "utf8");
530
+ }
531
+
532
+ // Save initial Doomsday Proof JSON alongside the file
533
+ const savedProofJsonPath = path.join(
534
+ path.dirname(filePath),
535
+ `reltime-doomsday-proof-${path.basename(filePath)}.json`
536
+ );
537
+ fs.writeFileSync(
538
+ savedProofJsonPath,
539
+ JSON.stringify(
540
+ {
541
+ protocol: "Rel-Time Sovereign Provenance Protocol",
542
+ file_name: path.basename(filePath),
543
+ identity_mode: args.biometric ? "BIOMETRIC_PASSKEY" : "ANONYMOUS",
544
+ original_file_sha256_hash: originalFileHash,
545
+ anchored_client_hash: clientHash,
546
+ signer_public_key: passkeyCreds ? passkeyCreds.public_key_hex : null,
547
+ t_index: tIndex,
548
+ vdf_ticks: data.vdf_ticks || data.solar_ticks,
549
+ anchor_mode: data.anchor_mode || anchorMode,
550
+ status: data.status || "pending_finality",
551
+ verify_url: verifyUrl,
552
+ sun_stamp_svg_url: stampEndpointUrl,
553
+ settlement: data.settlement || null,
554
+ },
555
+ null,
556
+ 2
557
+ ),
558
+ "utf8"
559
+ );
560
+
561
+ // 3. Automatically generate Stamped Copy (.PDF or .PNG) for PDFs and Images
562
+ let stampedCopyResult = null;
563
+ if (args.save_stamped_copy !== false) {
564
+ stampedCopyResult = await stampLocalFileOptionA(filePath, svgContent);
565
+ }
566
+
567
+ return {
568
+ status: data.status || "pending_finality",
569
+ file_name: path.basename(filePath),
570
+ identity_mode: args.biometric ? "Biometric Anchor (Passkey)" : "Anonymous Anchor",
571
+ original_file_sha256_hash: originalFileHash,
572
+ local_sha256_hash: clientHash,
573
+ signer_public_key: passkeyCreds ? passkeyCreds.public_key_hex : null,
574
+ t_index: tIndex,
575
+ vdf_ticks: data.vdf_ticks || data.solar_ticks,
576
+ anchor_mode: data.anchor_mode || anchorMode,
577
+ epoch_target: data.epoch_target,
578
+ verify_url: verifyUrl,
579
+ sun_stamp_svg_url: stampEndpointUrl,
580
+ saved_sun_stamp_svg_path: savedSvgPath,
581
+ saved_doomsday_proof_json_path: savedProofJsonPath,
582
+ stamped_copy: stampedCopyResult,
583
+ zero_knowledge_guarantee:
584
+ "File was hashed and stamped 100% locally on your machine; 0 bytes of file content were transmitted.",
585
+ };
586
+ }
587
+
588
+ if (name === "reltime_stamp_file") {
589
+ const filePath = path.resolve(args.file_path);
590
+ if (!fs.existsSync(filePath)) {
591
+ throw new Error(`File not found at path: ${filePath}`);
592
+ }
593
+ const targetHash = args.hash
594
+ ? args.hash.trim().toLowerCase()
595
+ : crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
596
+
597
+ if (!/^[0-9a-f]{64}$/.test(targetHash)) {
598
+ throw new Error("hash must be a valid 64-character hexadecimal SHA-256 string.");
599
+ }
600
+
601
+ const stampRes = await fetch(`${API_BASE}/stamp/${targetHash}`);
602
+ if (!stampRes.ok) {
603
+ throw new Error(`Failed to fetch Sun-Stamp (${stampRes.status}): ${await stampRes.text()}`);
604
+ }
605
+ const svgContent = await stampRes.text();
606
+ const result = await stampLocalFileOptionA(filePath, svgContent, args.output_path || null);
607
+ return {
608
+ file_path: filePath,
609
+ hash: targetHash,
610
+ verify_url: `https://rel-time.com/verify?t=${targetHash}`,
611
+ ...result,
612
+ };
613
+ }
614
+
615
+ if (name === "reltime_anchor_hash") {
616
+ if (!key) {
617
+ throw new Error("Missing RELTIME_API_KEY. Set RELTIME_API_KEY in your MCP config or pass `api_key`.");
618
+ }
619
+ const clientHash = (args.client_hash || "").trim().toLowerCase();
620
+ if (!/^[0-9a-f]{64}$/.test(clientHash)) {
621
+ throw new Error("client_hash must be a valid 64-character hexadecimal SHA-256 string.");
622
+ }
623
+ const anchorMode = args.anchor_mode || "standard";
624
+
625
+ const res = await fetch(`${API_BASE}/anchor`, {
626
+ method: "POST",
627
+ headers: {
628
+ "Content-Type": "application/json",
629
+ Authorization: `Bearer ${key}`,
630
+ },
631
+ body: JSON.stringify({
632
+ client_hash: clientHash,
633
+ anchor_mode: anchorMode,
634
+ }),
635
+ });
636
+
637
+ const data = await res.json();
638
+ if (!res.ok) {
639
+ throw new Error(`Rel-Time API error (${res.status}): ${JSON.stringify(data)}`);
640
+ }
641
+
642
+ const tIndex = data.t_index || data.thermodynamic_proof;
643
+ const lookupHash = anchorMode === "temporal" ? tIndex : clientHash;
644
+
645
+ return {
646
+ status: data.status,
647
+ client_hash: clientHash,
648
+ t_index: tIndex,
649
+ vdf_ticks: data.vdf_ticks || data.solar_ticks,
650
+ anchor_mode: data.anchor_mode || anchorMode,
651
+ epoch_target: data.epoch_target,
652
+ verify_url: `https://rel-time.com/verify?t=${lookupHash}`,
653
+ sun_stamp_svg_url: `${API_BASE}/stamp/${lookupHash}`,
654
+ };
655
+ }
656
+
657
+ if (name === "reltime_get_sun_stamp") {
658
+ const hash = (args.hash || "").trim().toLowerCase();
659
+ if (!/^[0-9a-f]{64}$/.test(hash)) {
660
+ throw new Error("hash must be a 64-character hexadecimal SHA-256 string.");
661
+ }
662
+ const stampUrl = `${API_BASE}/stamp/${hash}`;
663
+ const res = await fetch(stampUrl);
664
+ if (!res.ok) {
665
+ throw new Error(`Failed to fetch Sun-Stamp (${res.status}): ${await res.text()}`);
666
+ }
667
+ const svg = await res.text();
668
+ let savedPath = null;
669
+ if (args.output_svg_path) {
670
+ savedPath = path.resolve(args.output_svg_path);
671
+ fs.writeFileSync(savedPath, svg, "utf8");
672
+ }
673
+ return {
674
+ hash,
675
+ sun_stamp_svg_url: stampUrl,
676
+ verify_url: `https://rel-time.com/verify?t=${hash}`,
677
+ saved_svg_path: savedPath,
678
+ embed_markdown: `[![Anchored on Bitcoin via Rel-Time](${stampUrl})](https://rel-time.com/verify?t=${hash})`,
679
+ embed_html: `<a href="https://rel-time.com/verify?t=${hash}" target="_blank" rel="noopener"><img src="${stampUrl}" alt="Rel-Time Sun-Stamp" width="160" height="160" /></a>`,
680
+ };
681
+ }
682
+
683
+ if (name === "reltime_check_proof") {
684
+ const hash = (args.hash || "").trim().toLowerCase();
685
+ const res = await fetch(`${API_BASE}/proof/${hash}`);
686
+ const data = await res.json();
687
+ const out = {
688
+ http_status: res.status,
689
+ verify_url: `https://rel-time.com/verify?t=${hash}`,
690
+ ...data,
691
+ };
692
+ if (args.output_json_path) {
693
+ const jsonPath = path.resolve(args.output_json_path);
694
+ fs.writeFileSync(jsonPath, JSON.stringify(out, null, 2), "utf8");
695
+ out.saved_json_path = jsonPath;
696
+ }
697
+ return out;
698
+ }
699
+
700
+ if (name === "reltime_account_status") {
701
+ if (!key) {
702
+ throw new Error("Missing RELTIME_API_KEY.");
703
+ }
704
+ const res = await fetch(`${API_BASE}/account`, {
705
+ headers: { Authorization: `Bearer ${key}` },
706
+ });
707
+ const data = await res.json();
708
+ if (!res.ok) {
709
+ throw new Error(`Rel-Time Account error (${res.status}): ${JSON.stringify(data)}`);
710
+ }
711
+ return data;
712
+ }
713
+
714
+ if (name === "reltime_provision_agent_key") {
715
+ if (args.action === "create_invoice") {
716
+ const res = await fetch(`${API_BASE}/agent/invoice`, {
717
+ method: "POST",
718
+ headers: { "Content-Type": "application/json" },
719
+ body: JSON.stringify({
720
+ accept_terms: true,
721
+ pack: args.pack === "launch" ? "launch" : "standard",
722
+ }),
723
+ });
724
+ return await res.json();
725
+ } else if (args.action === "claim_invoice") {
726
+ if (!args.invoice_id) throw new Error("invoice_id is required when action is 'claim_invoice'.");
727
+ const claimPayload = {
728
+ invoice_id: args.invoice_id,
729
+ pack: args.pack === "launch" ? "launch" : undefined,
730
+ };
731
+ const targetReloadKey = args.api_key || (args.reload_existing ? CURRENT_API_KEY : "");
732
+ if (targetReloadKey && targetReloadKey.trim()) {
733
+ claimPayload.api_key = targetReloadKey.trim();
734
+ }
735
+ const res = await fetch(`${API_BASE}/agent/claim`, {
736
+ method: "POST",
737
+ headers: { "Content-Type": "application/json" },
738
+ body: JSON.stringify(claimPayload),
739
+ });
740
+ const data = await res.json();
741
+ if (data.api_key) {
742
+ CURRENT_API_KEY = data.api_key;
743
+ }
744
+ return data;
745
+ }
746
+ throw new Error(`Unknown action: ${args.action}`);
747
+ }
748
+
749
+ throw new Error(`Unknown tool: ${name}`);
750
+ }
751
+
752
+ // Standard MCP JSON-RPC 2.0 over stdio
753
+ const rl = readline.createInterface({
754
+ input: process.stdin,
755
+ terminal: false,
756
+ });
757
+
758
+ function sendJsonRpc(msg) {
759
+ process.stdout.write(JSON.stringify(msg) + "\n");
760
+ }
761
+
762
+ rl.on("line", async (line) => {
763
+ if (!line.trim()) return;
764
+ let req;
765
+ try {
766
+ req = JSON.parse(line);
767
+ } catch (e) {
768
+ return;
769
+ }
770
+
771
+ const { id, method, params } = req;
772
+
773
+ try {
774
+ if (method === "initialize") {
775
+ sendJsonRpc({
776
+ jsonrpc: "2.0",
777
+ id,
778
+ result: {
779
+ protocolVersion: (params && params.protocolVersion) || "2024-11-05",
780
+ capabilities: {
781
+ tools: {},
782
+ },
783
+ serverInfo: SERVER_INFO,
784
+ },
785
+ });
786
+ } else if (method === "notifications/initialized") {
787
+ // No response needed for notification
788
+ } else if (method === "tools/list") {
789
+ sendJsonRpc({
790
+ jsonrpc: "2.0",
791
+ id,
792
+ result: {
793
+ tools: TOOLS,
794
+ },
795
+ });
796
+ } else if (method === "tools/call") {
797
+ const toolName = params?.name;
798
+ const toolArgs = params?.arguments || {};
799
+ try {
800
+ const resultObj = await executeTool(toolName, toolArgs);
801
+ sendJsonRpc({
802
+ jsonrpc: "2.0",
803
+ id,
804
+ result: {
805
+ content: [
806
+ {
807
+ type: "text",
808
+ text: JSON.stringify(resultObj, null, 2),
809
+ },
810
+ ],
811
+ },
812
+ });
813
+ } catch (toolErr) {
814
+ sendJsonRpc({
815
+ jsonrpc: "2.0",
816
+ id,
817
+ result: {
818
+ isError: true,
819
+ content: [
820
+ {
821
+ type: "text",
822
+ text: `Error: ${toolErr.message || String(toolErr)}`,
823
+ },
824
+ ],
825
+ },
826
+ });
827
+ }
828
+ } else if (method === "ping") {
829
+ sendJsonRpc({ jsonrpc: "2.0", id, result: {} });
830
+ } else if (id !== undefined) {
831
+ sendJsonRpc({
832
+ jsonrpc: "2.0",
833
+ id,
834
+ error: { code: -32601, message: `Method not found: ${method}` },
835
+ });
836
+ }
837
+ } catch (err) {
838
+ if (id !== undefined) {
839
+ sendJsonRpc({
840
+ jsonrpc: "2.0",
841
+ id,
842
+ error: { code: -32603, message: err.message || "Internal error" },
843
+ });
844
+ }
845
+ }
846
+ });
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@rel-time/mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.2.0",
4
+ "description": "Official Model Context Protocol (MCP) server for Rel-Time (rel-time.com) — Zero-Knowledge Bitcoin & VDF Provenance, Local File Hashing, and Sun-Stamp Generation for Claude, Cursor, Windsurf, and Antigravity.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "mcp-server": "index.js",
8
+ "reltime-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "start": "node index.js"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "rel-time",
21
+ "bitcoin",
22
+ "provenance",
23
+ "timestamp",
24
+ "sun-stamp",
25
+ "vdf",
26
+ "passkey"
27
+ ],
28
+ "author": "Rel-Time Protocol (https://rel-time.com)",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "pdf-lib": "^1.17.1",
35
+ "sharp": "^0.35.5"
36
+ }
37
+ }