@rel-time/mcp-server 1.2.3 → 1.2.5
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/index.js +187 -31
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -17,7 +17,8 @@ const os = require("os");
|
|
|
17
17
|
const path = require("path");
|
|
18
18
|
const readline = require("readline");
|
|
19
19
|
const { exec } = require("child_process");
|
|
20
|
-
const { PDFDocument } = require("pdf-lib");
|
|
20
|
+
const { PDFDocument, PDFName, PDFString } = require("pdf-lib");
|
|
21
|
+
const QRCode = require("qrcode");
|
|
21
22
|
const sharp = require("sharp");
|
|
22
23
|
|
|
23
24
|
function normalizeApiBase(rawUrl) {
|
|
@@ -55,9 +56,38 @@ let LAST_INVOICE_PACK = null;
|
|
|
55
56
|
|
|
56
57
|
const SERVER_INFO = {
|
|
57
58
|
name: "rel-time-mcp-server",
|
|
58
|
-
version: "1.2.
|
|
59
|
+
version: "1.2.4",
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Resolves a local file path, automatically matching macOS screenshot filenames
|
|
64
|
+
* that contain Narrow No-Break Spaces (U+202F) or Non-Breaking Spaces (U+00A0)
|
|
65
|
+
* when the caller passes normal ASCII spaces.
|
|
66
|
+
*/
|
|
67
|
+
function resolveLocalFilePath(rawPath) {
|
|
68
|
+
const resolved = path.resolve(rawPath);
|
|
69
|
+
if (fs.existsSync(resolved)) {
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
const dir = path.dirname(resolved);
|
|
73
|
+
const targetBase = path.basename(resolved);
|
|
74
|
+
const normalizeSpaces = (s) => s.normalize("NFC").replace(/[\u00A0\u202F\u2007\u2009\s]+/g, " ");
|
|
75
|
+
const normalizedTarget = normalizeSpaces(targetBase);
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(dir)) {
|
|
78
|
+
const entries = fs.readdirSync(dir);
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (normalizeSpaces(entry) === normalizedTarget) {
|
|
81
|
+
return path.join(dir, entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Ignore directory read error
|
|
87
|
+
}
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
/**
|
|
62
92
|
* Loads saved API key from ~/.reltime/api_key.json if not provided via env/args.
|
|
63
93
|
*/
|
|
@@ -203,7 +233,7 @@ function openSystemBrowser(url) {
|
|
|
203
233
|
* Rel-Time WebAuthn Passkey Bridge in the user's browser so they can touch
|
|
204
234
|
* TouchID / FaceID / YubiKey.
|
|
205
235
|
*/
|
|
206
|
-
function launchBrowserPasskeyBridge(timeoutMs =
|
|
236
|
+
function launchBrowserPasskeyBridge(timeoutMs = 300000) {
|
|
207
237
|
return new Promise((resolve, reject) => {
|
|
208
238
|
const server = http.createServer((req, res) => {
|
|
209
239
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -252,10 +282,48 @@ function launchBrowserPasskeyBridge(timeoutMs = 120000) {
|
|
|
252
282
|
}
|
|
253
283
|
|
|
254
284
|
/**
|
|
255
|
-
*
|
|
285
|
+
* Injects a standard PNG tEXt chunk ("RelTimeVerify\0<verifyUrl>") right before IEND
|
|
286
|
+
* so Stamped PNG copies can be instantaneously verified when dropped into /verify.
|
|
287
|
+
*/
|
|
288
|
+
function injectPngVerifyBuffer(pngBuffer, verifyUrl) {
|
|
289
|
+
if (!verifyUrl || !Buffer.isBuffer(pngBuffer) || pngBuffer.length < 24) {
|
|
290
|
+
return pngBuffer;
|
|
291
|
+
}
|
|
292
|
+
const iendOffset = pngBuffer.length - 12;
|
|
293
|
+
if (pngBuffer.slice(iendOffset + 4, iendOffset + 8).toString("ascii") !== "IEND") {
|
|
294
|
+
return pngBuffer;
|
|
295
|
+
}
|
|
296
|
+
const payload = Buffer.from(`RelTimeVerify\0${verifyUrl}`, "utf8");
|
|
297
|
+
const typeBuf = Buffer.from("tEXt", "ascii");
|
|
298
|
+
const crcInput = Buffer.concat([typeBuf, payload]);
|
|
299
|
+
|
|
300
|
+
let crc = 0xffffffff;
|
|
301
|
+
for (let i = 0; i < crcInput.length; i++) {
|
|
302
|
+
crc ^= crcInput[i];
|
|
303
|
+
for (let b = 0; b < 8; b++) {
|
|
304
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
crc = (crc ^ 0xffffffff) >>> 0;
|
|
308
|
+
|
|
309
|
+
const lenBuf = Buffer.alloc(4);
|
|
310
|
+
lenBuf.writeUInt32BE(payload.length, 0);
|
|
311
|
+
const crcBuf = Buffer.alloc(4);
|
|
312
|
+
crcBuf.writeUInt32BE(crc, 0);
|
|
313
|
+
|
|
314
|
+
const chunk = Buffer.concat([lenBuf, crcInput, crcBuf]);
|
|
315
|
+
return Buffer.concat([
|
|
316
|
+
pngBuffer.slice(0, iendOffset),
|
|
317
|
+
chunk,
|
|
318
|
+
pngBuffer.slice(iendOffset),
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Stamps the Option 1A (+14° Bitcoin Tilt, pure crisp 64-ray Sun-Stamp, 0% glare, no pill) onto a local Image or PDF.
|
|
256
324
|
* Returns { stamped: true, output_path, format } or { stamped: false, reason }.
|
|
257
325
|
*/
|
|
258
|
-
async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = null) {
|
|
326
|
+
async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = null, verifyHash = null) {
|
|
259
327
|
const ext = path.extname(filePath).toLowerCase().replace(/^\./, "");
|
|
260
328
|
const dir = path.dirname(filePath);
|
|
261
329
|
const baseName = path.basename(filePath, path.extname(filePath));
|
|
@@ -270,18 +338,32 @@ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = nu
|
|
|
270
338
|
};
|
|
271
339
|
}
|
|
272
340
|
|
|
341
|
+
// Resolve verification URL from verifyHash or from <a href="..."> inside svgContent
|
|
342
|
+
let verifyUrl = verifyHash ? `https://rel-time.com/verify?t=${verifyHash.toLowerCase()}` : null;
|
|
343
|
+
if (!verifyUrl && typeof svgContent === "string") {
|
|
344
|
+
const match = svgContent.match(/https?:\/\/rel-time\.com\/verify\?[tThH]=([0-9a-fA-F]{64})/);
|
|
345
|
+
if (match) {
|
|
346
|
+
verifyUrl = `https://rel-time.com/verify?t=${match[1].toLowerCase()}`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Strip outer <a> wrapper before rasterizing with sharp/librsvg
|
|
351
|
+
const cleanSvg = String(svgContent || "")
|
|
352
|
+
.replace(/<a\b[^>]*>/gi, "")
|
|
353
|
+
.replace(/<\/a>/gi, "");
|
|
354
|
+
|
|
273
355
|
if (isImage) {
|
|
274
356
|
const outPath = customOutputPath ? path.resolve(customOutputPath) : path.join(dir, `reltime-stamped-${baseName}.png`);
|
|
275
357
|
const img = sharp(filePath);
|
|
276
358
|
const meta = await img.metadata();
|
|
277
|
-
const w = meta.width || 1200;
|
|
278
|
-
const h = meta.height || 800;
|
|
359
|
+
const w = (meta.autoOrient && meta.autoOrient.width) || meta.width || 1200;
|
|
360
|
+
const h = (meta.autoOrient && meta.autoOrient.height) || meta.height || 800;
|
|
279
361
|
|
|
280
362
|
const minDim = Math.min(w, h);
|
|
281
|
-
const badgeSize = Math.max(
|
|
282
|
-
const margin = Math.max(
|
|
363
|
+
const badgeSize = Math.max(180, Math.round(minDim * 0.22));
|
|
364
|
+
const margin = Math.max(14, Math.round(minDim * 0.025));
|
|
283
365
|
|
|
284
|
-
const stampPngBuffer = await sharp(Buffer.from(
|
|
366
|
+
const stampPngBuffer = await sharp(Buffer.from(cleanSvg, "utf8"))
|
|
285
367
|
.resize(badgeSize, badgeSize)
|
|
286
368
|
.png()
|
|
287
369
|
.toBuffer();
|
|
@@ -289,10 +371,14 @@ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = nu
|
|
|
289
371
|
const left = Math.max(0, w - badgeSize - margin);
|
|
290
372
|
const top = Math.max(0, h - badgeSize - margin);
|
|
291
373
|
|
|
292
|
-
await
|
|
374
|
+
const rawOutBuf = await sharp(filePath)
|
|
375
|
+
.rotate()
|
|
293
376
|
.composite([{ input: stampPngBuffer, left, top }])
|
|
294
377
|
.png()
|
|
295
|
-
.
|
|
378
|
+
.toBuffer();
|
|
379
|
+
|
|
380
|
+
const finalPngBuf = verifyUrl ? injectPngVerifyBuffer(rawOutBuf, verifyUrl) : rawOutBuf;
|
|
381
|
+
fs.writeFileSync(outPath, finalPngBuf);
|
|
296
382
|
|
|
297
383
|
return {
|
|
298
384
|
stamped: true,
|
|
@@ -313,24 +399,52 @@ async function stampLocalFileOptionA(filePath, svgContent, customOutputPath = nu
|
|
|
313
399
|
const firstPage = pages[0];
|
|
314
400
|
const { width, height } = firstPage.getSize();
|
|
315
401
|
|
|
316
|
-
// Render high-DPI
|
|
317
|
-
const stampPngBuffer = await sharp(Buffer.from(
|
|
318
|
-
.resize(
|
|
402
|
+
// Render high-DPI 1024x1024 (300+ DPI) Option 1A Sun-Stamp PNG
|
|
403
|
+
const stampPngBuffer = await sharp(Buffer.from(cleanSvg, "utf8"))
|
|
404
|
+
.resize(1024, 1024)
|
|
319
405
|
.png()
|
|
320
406
|
.toBuffer();
|
|
321
407
|
|
|
322
408
|
const pngImage = await pdfDoc.embedPng(stampPngBuffer);
|
|
323
|
-
const targetSize = Math.min(
|
|
409
|
+
const targetSize = Math.min(148, Math.max(105, Math.min(width, height) * 0.22));
|
|
324
410
|
const margin = 18;
|
|
411
|
+
const drawX = Math.max(margin, width - targetSize - margin);
|
|
412
|
+
const drawY = margin;
|
|
325
413
|
|
|
326
414
|
firstPage.drawImage(pngImage, {
|
|
327
|
-
x:
|
|
328
|
-
y:
|
|
415
|
+
x: drawX,
|
|
416
|
+
y: drawY,
|
|
329
417
|
width: targetSize,
|
|
330
418
|
height: targetSize,
|
|
331
419
|
});
|
|
332
420
|
|
|
333
|
-
|
|
421
|
+
// Attach clickable PDF Link Annotation over the Sun-Stamp rectangle
|
|
422
|
+
if (verifyUrl) {
|
|
423
|
+
try {
|
|
424
|
+
const linkAnnotation = pdfDoc.context.obj({
|
|
425
|
+
Type: "Annot",
|
|
426
|
+
Subtype: "Link",
|
|
427
|
+
Rect: [drawX, drawY, drawX + targetSize, drawY + targetSize],
|
|
428
|
+
Border: [0, 0, 0],
|
|
429
|
+
F: 4,
|
|
430
|
+
A: {
|
|
431
|
+
Type: "Action",
|
|
432
|
+
S: "URI",
|
|
433
|
+
URI: PDFString.of(verifyUrl),
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
const linkRef = pdfDoc.context.register(linkAnnotation);
|
|
437
|
+
if (typeof firstPage.node.addAnnot === "function") {
|
|
438
|
+
firstPage.node.addAnnot(linkRef);
|
|
439
|
+
} else {
|
|
440
|
+
firstPage.node.set(PDFName.of("Annots"), pdfDoc.context.obj([linkRef]));
|
|
441
|
+
}
|
|
442
|
+
} catch (e) {
|
|
443
|
+
// Ignore annotation error on restricted PDFs
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false });
|
|
334
448
|
fs.writeFileSync(outPath, Buffer.from(modifiedPdfBytes));
|
|
335
449
|
|
|
336
450
|
return {
|
|
@@ -503,7 +617,7 @@ const TOOLS = [
|
|
|
503
617
|
{
|
|
504
618
|
name: "reltime_provision_agent_key",
|
|
505
619
|
description:
|
|
506
|
-
"Self-onboarding & Top-Up for Autonomous AI Agents via Bitcoin Lightning (`POST /v1/agent/invoice` & `POST /v1/agent/claim`). IMPORTANT
|
|
620
|
+
"Self-onboarding & Top-Up for Autonomous AI Agents via Bitcoin Lightning (`POST /v1/agent/invoice` & `POST /v1/agent/claim`). IMPORTANT:\n1. Before calling `create_invoice` for a new key, ALWAYS ask the user which pack they want first:\n - **Agent Launch Pack (`pack: 'launch'`, $5 / 50 Hashes):** Priority 10-Minute Bitcoin Queue · Standard Mode only (Data Integrity + Biometric or Anonymous) · Includes Sun-Stamp, 1-Click Stamped Copy (.PDF/.PNG), and .JSON Doomsday Proof.\n - **Autonomous Agents (`pack: 'standard'`, $50 / 500 Hashes):** Dynamic 6-Hour Batch Queue · Supports both Temporal Mode (`T_index` sub-second sequencing) and Standard Mode · Reloadable anytime (+500 credits / $50) with zero key rotation.\n2. When `create_invoice` returns, ALWAYS present BOTH the scannable QR code image (by calling Read on `qr_code_png_path` so the image renders inline in chat!) AND the full, un-truncated `ln_invoice` (`bolt11`) string in a copyable code block on the VERY FIRST try!",
|
|
507
621
|
inputSchema: {
|
|
508
622
|
type: "object",
|
|
509
623
|
properties: {
|
|
@@ -576,7 +690,7 @@ async function executeTool(name, args = {}) {
|
|
|
576
690
|
"Before anchoring files, a Rel-Time API key is required. Ask the user which pack they want to provision first:"
|
|
577
691
|
);
|
|
578
692
|
}
|
|
579
|
-
const filePath =
|
|
693
|
+
const filePath = resolveLocalFilePath(args.file_path);
|
|
580
694
|
if (!fs.existsSync(filePath)) {
|
|
581
695
|
throw new Error(`File not found at path: ${filePath}`);
|
|
582
696
|
}
|
|
@@ -671,7 +785,7 @@ async function executeTool(name, args = {}) {
|
|
|
671
785
|
// 3. Automatically generate Stamped Copy (.PDF or .PNG) for PDFs and Images
|
|
672
786
|
let stampedCopyResult = null;
|
|
673
787
|
if (args.save_stamped_copy !== false) {
|
|
674
|
-
stampedCopyResult = await stampLocalFileOptionA(filePath, svgContent);
|
|
788
|
+
stampedCopyResult = await stampLocalFileOptionA(filePath, svgContent, null, clientHash);
|
|
675
789
|
}
|
|
676
790
|
|
|
677
791
|
return {
|
|
@@ -696,7 +810,7 @@ async function executeTool(name, args = {}) {
|
|
|
696
810
|
}
|
|
697
811
|
|
|
698
812
|
if (name === "reltime_stamp_file") {
|
|
699
|
-
const filePath =
|
|
813
|
+
const filePath = resolveLocalFilePath(args.file_path);
|
|
700
814
|
if (!fs.existsSync(filePath)) {
|
|
701
815
|
throw new Error(`File not found at path: ${filePath}`);
|
|
702
816
|
}
|
|
@@ -713,7 +827,7 @@ async function executeTool(name, args = {}) {
|
|
|
713
827
|
throw new Error(`Failed to fetch Sun-Stamp (${stampRes.status}): ${await stampRes.text()}`);
|
|
714
828
|
}
|
|
715
829
|
const svgContent = await stampRes.text();
|
|
716
|
-
const result = await stampLocalFileOptionA(filePath, svgContent, args.output_path || null);
|
|
830
|
+
const result = await stampLocalFileOptionA(filePath, svgContent, args.output_path || null, targetHash);
|
|
717
831
|
return {
|
|
718
832
|
file_path: filePath,
|
|
719
833
|
hash: targetHash,
|
|
@@ -853,7 +967,40 @@ async function executeTool(name, args = {}) {
|
|
|
853
967
|
pack: args.pack,
|
|
854
968
|
}),
|
|
855
969
|
});
|
|
856
|
-
|
|
970
|
+
const data = await res.json();
|
|
971
|
+
const bolt11 = data.ln_invoice || data.bolt11 || "";
|
|
972
|
+
if (bolt11) {
|
|
973
|
+
try {
|
|
974
|
+
if (!fs.existsSync(PASSKEY_CONFIG_DIR)) {
|
|
975
|
+
fs.mkdirSync(PASSKEY_CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
976
|
+
}
|
|
977
|
+
const qrPngBuffer = await QRCode.toBuffer(bolt11.toUpperCase(), {
|
|
978
|
+
type: "png",
|
|
979
|
+
width: 560,
|
|
980
|
+
margin: 2,
|
|
981
|
+
errorCorrectionLevel: "M",
|
|
982
|
+
});
|
|
983
|
+
const homeQrPath = path.join(PASSKEY_CONFIG_DIR, "lightning-invoice-qr.png");
|
|
984
|
+
const tmpQrPath = "/tmp/reltime-lightning-qr.png";
|
|
985
|
+
fs.writeFileSync(homeQrPath, qrPngBuffer);
|
|
986
|
+
try {
|
|
987
|
+
fs.writeFileSync(tmpQrPath, qrPngBuffer);
|
|
988
|
+
} catch (e) {
|
|
989
|
+
// Ignore /tmp write error
|
|
990
|
+
}
|
|
991
|
+
data.bolt11 = bolt11;
|
|
992
|
+
data.ln_invoice = bolt11;
|
|
993
|
+
data.qr_code_png_path = homeQrPath;
|
|
994
|
+
data.qr_code_tmp_png_path = tmpQrPath;
|
|
995
|
+
data.invoice_expiration = "60 minutes (3,600 seconds)";
|
|
996
|
+
data.mandatory_first_reply_instructions =
|
|
997
|
+
"CRITICAL INSTRUCTION FOR AI AGENT: On this very first reply, you MUST show the user BOTH: (1) the scannable QR code image (immediately call your Read tool on `qr_code_png_path` BEFORE sending your message so the QR code PNG renders inline in the chat window right now!) AND (2) the full, un-truncated `bolt11` (`lnbc...`) Lightning invoice string inside a copyable code block so the user can either scan with their phone or copy-paste on the first try without asking!";
|
|
998
|
+
data._mcp_image_base64 = qrPngBuffer.toString("base64");
|
|
999
|
+
} catch (qrErr) {
|
|
1000
|
+
data.qr_error = qrErr.message;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return data;
|
|
857
1004
|
} else if (args.action === "claim_invoice") {
|
|
858
1005
|
if (!args.invoice_id) throw new Error("invoice_id is required when action is 'claim_invoice'.");
|
|
859
1006
|
const resolvedPack = args.pack || LAST_INVOICE_PACK || undefined;
|
|
@@ -933,16 +1080,25 @@ rl.on("line", async (line) => {
|
|
|
933
1080
|
const toolArgs = params?.arguments || {};
|
|
934
1081
|
try {
|
|
935
1082
|
const resultObj = await executeTool(toolName, toolArgs);
|
|
1083
|
+
const contentItems = [];
|
|
1084
|
+
if (resultObj && resultObj._mcp_image_base64) {
|
|
1085
|
+
const b64 = resultObj._mcp_image_base64;
|
|
1086
|
+
delete resultObj._mcp_image_base64;
|
|
1087
|
+
contentItems.push({
|
|
1088
|
+
type: "image",
|
|
1089
|
+
data: b64,
|
|
1090
|
+
mimeType: "image/png",
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
contentItems.push({
|
|
1094
|
+
type: "text",
|
|
1095
|
+
text: JSON.stringify(resultObj, null, 2),
|
|
1096
|
+
});
|
|
936
1097
|
sendJsonRpc({
|
|
937
1098
|
jsonrpc: "2.0",
|
|
938
1099
|
id,
|
|
939
1100
|
result: {
|
|
940
|
-
content:
|
|
941
|
-
{
|
|
942
|
-
type: "text",
|
|
943
|
-
text: JSON.stringify(resultObj, null, 2),
|
|
944
|
-
},
|
|
945
|
-
],
|
|
1101
|
+
content: contentItems,
|
|
946
1102
|
},
|
|
947
1103
|
});
|
|
948
1104
|
} catch (toolErr) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rel-time/mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
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
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"pdf-lib": "^1.17.1",
|
|
35
|
+
"qrcode": "^1.5.4",
|
|
35
36
|
"sharp": "^0.35.5"
|
|
36
37
|
}
|
|
37
38
|
}
|