@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.
Files changed (2) hide show
  1. package/index.js +187 -31
  2. 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.3",
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 = 120000) {
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
- * Stamps the Option A (pure crisp 64-ray Sun-Stamp, 0% glare, no pill) onto a local Image or PDF.
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(130, Math.min(420, Math.round(minDim * 0.22)));
282
- const margin = Math.max(12, Math.round(minDim * 0.025));
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(svgContent, "utf8"))
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 img
374
+ const rawOutBuf = await sharp(filePath)
375
+ .rotate()
293
376
  .composite([{ input: stampPngBuffer, left, top }])
294
377
  .png()
295
- .toFile(outPath);
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 480x480 Option A Sun-Stamp PNG (pure crisp SVG, 0% glare, no pill)
317
- const stampPngBuffer = await sharp(Buffer.from(svgContent, "utf8"))
318
- .resize(480, 480)
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(135, Math.max(85, Math.min(width, height) * 0.2));
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: Math.max(margin, width - targetSize - margin),
328
- y: margin,
415
+ x: drawX,
416
+ y: drawY,
329
417
  width: targetSize,
330
418
  height: targetSize,
331
419
  });
332
420
 
333
- const modifiedPdfBytes = await pdfDoc.save();
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: Before calling `create_invoice` for a new key, ALWAYS ask the user which pack they want first:\n1. **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.\n2. **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.",
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 = path.resolve(args.file_path);
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 = path.resolve(args.file_path);
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
- return await res.json();
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",
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
  }