@caravo/mcp 0.1.27 → 0.1.29

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/dist/index.js +123 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -35,6 +35,16 @@ checkForUpdate("@caravo/mcp", VERSION).then((info) => {
35
35
  process.stderr.write(`[caravo] update available: ${info.current} → ${info.latest} (will auto-update on next restart)\n`);
36
36
  }
37
37
  });
38
+ /** Append update notice to the last text content block in a tool response */
39
+ function appendUpdateNotice(result) {
40
+ if (!pendingUpdate)
41
+ return result;
42
+ const lastText = [...result.content].reverse().find((c) => c.type === "text" && c.text);
43
+ if (lastText && lastText.text) {
44
+ lastText.text += `\n\n⚠ Caravo MCP update available: ${pendingUpdate.current} → ${pendingUpdate.latest}. Restart the MCP server to auto-update.`;
45
+ }
46
+ return result;
47
+ }
38
48
  // ─── Shared description blocks (used in both `instructions` and `search_tools`) ─
39
49
  const ABOUT_CARAVO = "Caravo (https://caravo.ai) — the API marketplace for AI agents. 200+ tools at $0.001–$0.05/call.\n" +
40
50
  "Docs & source: https://github.com/Caravo-AI/Caravo-MCP";
@@ -246,34 +256,60 @@ function stripDangerousFields(input) {
246
256
  }
247
257
  return cleaned;
248
258
  }
259
+ const MIME_MAP = {
260
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
261
+ ".gif": "image/gif", ".webp": "image/webp", ".bmp": "image/bmp",
262
+ ".svg": "image/svg+xml", ".tif": "image/tiff", ".tiff": "image/tiff",
263
+ ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/quicktime",
264
+ ".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg",
265
+ ".pdf": "application/pdf",
266
+ };
267
+ const FILE_EXTENSIONS = new Set(Object.keys(MIME_MAP));
249
268
  /**
250
- * Resolve local file paths in tool input to base64.
251
- * Detects file:// URIs and absolute paths with image extensions.
252
- * Runs locally so base64 never enters the LLM context.
269
+ * Resolve local file paths in tool input to data URIs.
270
+ * Detects file:// URIs, absolute paths, ~/ and ./ paths with known extensions.
271
+ * Data URIs preserve MIME type so the server can upload to Fal Storage.
253
272
  */
254
273
  function resolveLocalFiles(input) {
255
274
  const result = { ...input };
256
275
  for (const [key, value] of Object.entries(result)) {
257
276
  if (typeof value !== "string")
258
277
  continue;
278
+ // Skip URLs and existing data URIs
279
+ if (/^https?:\/\//i.test(value) || /^data:/i.test(value))
280
+ continue;
259
281
  const filePath = toLocalPath(value);
260
282
  if (!filePath)
261
283
  continue;
262
284
  if (!existsSync(filePath)) {
263
285
  throw new Error(`Local file not found: ${filePath}`);
264
286
  }
265
- result[key] = readFileSync(filePath).toString("base64");
287
+ const data = readFileSync(filePath);
288
+ const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
289
+ const mime = MIME_MAP[ext] || "application/octet-stream";
290
+ result[key] = `data:${mime};base64,${data.toString("base64")}`;
291
+ process.stderr.write(`[caravo] file → data URI: ${filePath} (${mime}, ${data.length} bytes)\n`);
266
292
  }
267
293
  return result;
268
294
  }
269
295
  function toLocalPath(value) {
270
- if (value.startsWith("file:///"))
271
- return value.slice(7);
272
- if (value.startsWith("file://"))
273
- return value.slice(7);
274
- if (/^\//.test(value) && /\.(png|jpe?g|gif|webp|bmp|svg|tiff?)$/i.test(value))
275
- return value;
276
- return null;
296
+ let path = null;
297
+ if (value.startsWith("file://")) {
298
+ path = value.slice(7);
299
+ }
300
+ else if (value.startsWith("~/")) {
301
+ path = join(homedir(), value.slice(2));
302
+ }
303
+ else if (value.startsWith("./") || value.startsWith("../")) {
304
+ path = join(process.cwd(), value);
305
+ }
306
+ else if (/^\//.test(value)) {
307
+ path = value;
308
+ }
309
+ if (!path)
310
+ return null;
311
+ const ext = path.substring(path.lastIndexOf(".")).toLowerCase();
312
+ return FILE_EXTENSIONS.has(ext) ? path : null;
277
313
  }
278
314
  // ─── Favorites registration ────────────────────────────────────────────────────
279
315
  // Track registered fav tool handles for dynamic add/remove
@@ -374,7 +410,7 @@ function makeFavToolHandler(tool) {
374
410
  const { dry_run, ...rawInput } = args;
375
411
  const toolInput = resolveLocalFiles(rawInput);
376
412
  if (dry_run) {
377
- return dryRunProbe(tool.id, toolInput);
413
+ return appendUpdateNotice(await dryRunProbe(tool.id, toolInput));
378
414
  }
379
415
  try {
380
416
  const result = await apiPost(`/api/tools/${tool.id}/execute`, toolInput);
@@ -387,26 +423,26 @@ function makeFavToolHandler(tool) {
387
423
  ...formatOutput(result.output),
388
424
  ...reviewLines,
389
425
  ];
390
- return {
426
+ return appendUpdateNotice({
391
427
  content: [{ type: "text", text: lines.join("\n") }],
392
- };
428
+ });
393
429
  }
394
430
  if (result.x402Version || result.accepts) {
395
431
  const price = `$${tool.pricing.price_per_call}`;
396
- return {
432
+ return appendUpdateNotice({
397
433
  content: [{ type: "text", text: buildPaymentRequiredMessage(price) }],
398
434
  isError: true,
399
- };
435
+ });
400
436
  }
401
- return {
437
+ return appendUpdateNotice({
402
438
  content: [
403
439
  { type: "text", text: `Error: ${safeJsonText(result, false)}` },
404
440
  ],
405
441
  isError: true,
406
- };
442
+ });
407
443
  }
408
444
  catch (err) {
409
- return {
445
+ return appendUpdateNotice({
410
446
  content: [
411
447
  {
412
448
  type: "text",
@@ -414,7 +450,7 @@ function makeFavToolHandler(tool) {
414
450
  },
415
451
  ],
416
452
  isError: true,
417
- };
453
+ });
418
454
  }
419
455
  };
420
456
  }
@@ -555,19 +591,15 @@ function registerAllTools(server) {
555
591
  params.set("per_page", String(per_page));
556
592
  params.set("view", "agent");
557
593
  const data = await apiGet(`/api/tools?${params}`);
558
- let text = safeJsonText(data);
559
- if (pendingUpdate) {
560
- text += `\n\n[Update available: @caravo/mcp ${pendingUpdate.current} → ${pendingUpdate.latest}. Will auto-update on next MCP restart.]`;
561
- }
562
- return {
563
- content: [{ type: "text", text }],
564
- };
594
+ return appendUpdateNotice({
595
+ content: [{ type: "text", text: safeJsonText(data) }],
596
+ });
565
597
  }
566
598
  catch (err) {
567
- return {
599
+ return appendUpdateNotice({
568
600
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
569
601
  isError: true,
570
- };
602
+ });
571
603
  }
572
604
  });
573
605
  // ── Get tool info ────────────────────────────────────────────────────────────
@@ -586,22 +618,22 @@ function registerAllTools(server) {
586
618
  }
587
619
  try {
588
620
  const data = await apiGet(`/api/tools/${tool_id.trim()}`);
589
- return {
621
+ return appendUpdateNotice({
590
622
  content: [{ type: "text", text: safeJsonText(data) }],
591
- };
623
+ });
592
624
  }
593
625
  catch (err) {
594
- return {
626
+ return appendUpdateNotice({
595
627
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
596
628
  isError: true,
597
- };
629
+ });
598
630
  }
599
631
  });
600
632
  // ── use_tool (meta-tool) ─────────────────────────────────────────────────────
601
633
  server.registerTool("use_tool", {
602
634
  description: "Execute any marketplace tool by ID. Use get_tool_info first to see the required input schema. " +
603
635
  "Paid tools auto-pay via x402 (wallet) or API key balance. " +
604
- "File upload tip: For tools that accept file input, you can pass a local file path (e.g., /path/to/photo.jpg) or file:// URI — it will be auto-converted to base64. Prefer passing a URL when available. " +
636
+ "File upload tip: For tools that accept file input, you can pass a local file path (e.g., /path/to/photo.jpg), ~/path, or file:// URI — it will be auto-uploaded to CDN. Prefer passing a URL when available. " +
605
637
  "After using a tool, check existing reviews first — upvote one if it matches your experience, or write a new review if none captures your feedback.",
606
638
  inputSchema: {
607
639
  tool_id: z.string().describe("The tool ID or slug to execute (e.g., 'black-forest-labs/flux.1-schnell' or 'alice/imagen-4')"),
@@ -613,15 +645,15 @@ function registerAllTools(server) {
613
645
  }, async ({ tool_id, input, dry_run }) => {
614
646
  const validationError = validateToolId(tool_id);
615
647
  if (validationError) {
616
- return {
648
+ return appendUpdateNotice({
617
649
  content: [{ type: "text", text: `Error: ${validationError}` }],
618
650
  isError: true,
619
- };
651
+ });
620
652
  }
621
653
  const cleanInput = resolveLocalFiles(stripDangerousFields(input));
622
654
  // Dry-run mode: probe cost without executing or paying
623
655
  if (dry_run) {
624
- return dryRunProbe(tool_id.trim(), cleanInput);
656
+ return appendUpdateNotice(await dryRunProbe(tool_id.trim(), cleanInput));
625
657
  }
626
658
  try {
627
659
  const result = await apiPost(`/api/tools/${tool_id.trim()}/execute`, cleanInput);
@@ -634,28 +666,28 @@ function registerAllTools(server) {
634
666
  ...formatOutput(result.output),
635
667
  ...reviewLines,
636
668
  ];
637
- return {
669
+ return appendUpdateNotice({
638
670
  content: [{ type: "text", text: lines.join("\n") }],
639
- };
671
+ });
640
672
  }
641
673
  if (result.x402Version || result.accepts) {
642
674
  const price = result.accepts?.[0]?.amount
643
675
  ? `$${(parseInt(result.accepts[0].amount) / 1e6).toFixed(6)}`
644
676
  : "?";
645
- return {
677
+ return appendUpdateNotice({
646
678
  content: [{ type: "text", text: buildPaymentRequiredMessage(price) }],
647
679
  isError: true,
648
- };
680
+ });
649
681
  }
650
- return {
682
+ return appendUpdateNotice({
651
683
  content: [
652
684
  { type: "text", text: `Error: ${safeJsonText(result, false)}` },
653
685
  ],
654
686
  isError: true,
655
- };
687
+ });
656
688
  }
657
689
  catch (err) {
658
- return {
690
+ return appendUpdateNotice({
659
691
  content: [
660
692
  {
661
693
  type: "text",
@@ -663,7 +695,7 @@ function registerAllTools(server) {
663
695
  },
664
696
  ],
665
697
  isError: true,
666
- };
698
+ });
667
699
  }
668
700
  });
669
701
  // ── Submit review / upvote ───────────────────────────────────────────────────
@@ -717,12 +749,12 @@ function registerAllTools(server) {
717
749
  lines.push(`This was a 5/5 review — consider saving tool_id="${upvotedToolId}" to your memory for future reuse.`);
718
750
  }
719
751
  }
720
- return { content: [{ type: "text", text: lines.join("\n") }] };
752
+ return appendUpdateNotice({ content: [{ type: "text", text: lines.join("\n") }] });
721
753
  }
722
- return {
754
+ return appendUpdateNotice({
723
755
  content: [{ type: "text", text: result.error ? `Error: ${result.error}` : safeJsonText(result) }],
724
756
  isError: true,
725
- };
757
+ });
726
758
  }
727
759
  // New review mode
728
760
  if (rating == null || !comment) {
@@ -749,10 +781,10 @@ function registerAllTools(server) {
749
781
  agent_id,
750
782
  });
751
783
  if (result.error) {
752
- return {
784
+ return appendUpdateNotice({
753
785
  content: [{ type: "text", text: `Error: ${result.error}` }],
754
786
  isError: true,
755
- };
787
+ });
756
788
  }
757
789
  // The API returns the review record with tool_id derived from execution
758
790
  const reviewToolId = result.tool_id || "unknown";
@@ -767,13 +799,13 @@ function registerAllTools(server) {
767
799
  lines.push(`This tool scored 5/5 — consider saving tool_id="${reviewToolId}" to your memory for future reuse.`);
768
800
  }
769
801
  }
770
- return { content: [{ type: "text", text: lines.join("\n") }] };
802
+ return appendUpdateNotice({ content: [{ type: "text", text: lines.join("\n") }] });
771
803
  }
772
804
  catch (err) {
773
- return {
805
+ return appendUpdateNotice({
774
806
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
775
807
  isError: true,
776
- };
808
+ });
777
809
  }
778
810
  });
779
811
  // ── Wallet info ──────────────────────────────────────────────────────────────
@@ -835,14 +867,14 @@ function registerAllTools(server) {
835
867
  else {
836
868
  info.note = "Send USDC on Base to this address to enable automatic x402 payments.";
837
869
  }
838
- return {
870
+ return appendUpdateNotice({
839
871
  content: [
840
872
  {
841
873
  type: "text",
842
874
  text: JSON.stringify(info, null, 2),
843
875
  },
844
876
  ],
845
- };
877
+ });
846
878
  });
847
879
  // ── Login (browser-based account connect) ────────────────────────────────────
848
880
  server.registerTool("login", {
@@ -974,15 +1006,15 @@ function registerAllTools(server) {
974
1006
  }, async () => {
975
1007
  try {
976
1008
  const data = await apiGet("/api/tags");
977
- return {
1009
+ return appendUpdateNotice({
978
1010
  content: [{ type: "text", text: safeJsonText(data) }],
979
- };
1011
+ });
980
1012
  }
981
1013
  catch (err) {
982
- return {
1014
+ return appendUpdateNotice({
983
1015
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
984
1016
  isError: true,
985
- };
1017
+ });
986
1018
  }
987
1019
  });
988
1020
  // ── List providers ───────────────────────────────────────────────────────────
@@ -992,15 +1024,15 @@ function registerAllTools(server) {
992
1024
  }, async () => {
993
1025
  try {
994
1026
  const data = await apiGet("/api/providers");
995
- return {
1027
+ return appendUpdateNotice({
996
1028
  content: [{ type: "text", text: safeJsonText(data) }],
997
- };
1029
+ });
998
1030
  }
999
1031
  catch (err) {
1000
- return {
1032
+ return appendUpdateNotice({
1001
1033
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1002
1034
  isError: true,
1003
- };
1035
+ });
1004
1036
  }
1005
1037
  });
1006
1038
  // ── Tool Requests ───────────────────────────────────────────────────────────
@@ -1027,15 +1059,15 @@ function registerAllTools(server) {
1027
1059
  params.set("page", String(page));
1028
1060
  params.set("per_page", String(per_page));
1029
1061
  const data = await apiGet(`/api/tool-requests?${params}`);
1030
- return {
1062
+ return appendUpdateNotice({
1031
1063
  content: [{ type: "text", text: safeJsonText(data) }],
1032
- };
1064
+ });
1033
1065
  }
1034
1066
  catch (err) {
1035
- return {
1067
+ return appendUpdateNotice({
1036
1068
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1037
1069
  isError: true,
1038
- };
1070
+ });
1039
1071
  }
1040
1072
  });
1041
1073
  server.registerTool("request_tool", {
@@ -1059,12 +1091,12 @@ function registerAllTools(server) {
1059
1091
  agent_id,
1060
1092
  });
1061
1093
  if (result.error) {
1062
- return {
1094
+ return appendUpdateNotice({
1063
1095
  content: [{ type: "text", text: `Error: ${result.error}` }],
1064
1096
  isError: true,
1065
- };
1097
+ });
1066
1098
  }
1067
- return {
1099
+ return appendUpdateNotice({
1068
1100
  content: [
1069
1101
  {
1070
1102
  type: "text",
@@ -1077,13 +1109,13 @@ function registerAllTools(server) {
1077
1109
  ].join("\n"),
1078
1110
  },
1079
1111
  ],
1080
- };
1112
+ });
1081
1113
  }
1082
1114
  catch (err) {
1083
- return {
1115
+ return appendUpdateNotice({
1084
1116
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1085
1117
  isError: true,
1086
- };
1118
+ });
1087
1119
  }
1088
1120
  });
1089
1121
  server.registerTool("upvote_tool_request", {
@@ -1099,21 +1131,21 @@ function registerAllTools(server) {
1099
1131
  execution_id,
1100
1132
  });
1101
1133
  if (result.error) {
1102
- return {
1134
+ return appendUpdateNotice({
1103
1135
  content: [{ type: "text", text: `Error: ${result.error}` }],
1104
1136
  isError: true,
1105
- };
1137
+ });
1106
1138
  }
1107
1139
  const action = result.action === "already_upvoted" ? "Already upvoted" : "Upvoted";
1108
- return {
1140
+ return appendUpdateNotice({
1109
1141
  content: [{ type: "text", text: `✓ ${action} tool request ${request_id}` }],
1110
- };
1142
+ });
1111
1143
  }
1112
1144
  catch (err) {
1113
- return {
1145
+ return appendUpdateNotice({
1114
1146
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1115
1147
  isError: true,
1116
- };
1148
+ });
1117
1149
  }
1118
1150
  });
1119
1151
  // ── Favorites management ─────────────────────────────────────────────────────
@@ -1141,7 +1173,7 @@ function registerAllTools(server) {
1141
1173
  };
1142
1174
  }
1143
1175
  const tools = result.data ?? [];
1144
- return {
1176
+ return appendUpdateNotice({
1145
1177
  content: [
1146
1178
  {
1147
1179
  type: "text",
@@ -1157,13 +1189,13 @@ function registerAllTools(server) {
1157
1189
  }),
1158
1190
  },
1159
1191
  ],
1160
- };
1192
+ });
1161
1193
  }
1162
1194
  catch (err) {
1163
- return {
1195
+ return appendUpdateNotice({
1164
1196
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1165
1197
  isError: true,
1166
- };
1198
+ });
1167
1199
  }
1168
1200
  });
1169
1201
  server.registerTool("favorite_tool", {
@@ -1200,7 +1232,7 @@ function registerAllTools(server) {
1200
1232
  if (tool) {
1201
1233
  registerFavTool(server, tool);
1202
1234
  }
1203
- return {
1235
+ return appendUpdateNotice({
1204
1236
  content: [
1205
1237
  {
1206
1238
  type: "text",
@@ -1212,13 +1244,13 @@ function registerAllTools(server) {
1212
1244
  ].join("\n"),
1213
1245
  },
1214
1246
  ],
1215
- };
1247
+ });
1216
1248
  }
1217
1249
  catch (err) {
1218
- return {
1250
+ return appendUpdateNotice({
1219
1251
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1220
1252
  isError: true,
1221
- };
1253
+ });
1222
1254
  }
1223
1255
  });
1224
1256
  server.registerTool("unfavorite_tool", {
@@ -1253,7 +1285,7 @@ function registerAllTools(server) {
1253
1285
  registered.remove();
1254
1286
  registeredFavTools.delete(tool_id);
1255
1287
  }
1256
- return {
1288
+ return appendUpdateNotice({
1257
1289
  content: [
1258
1290
  {
1259
1291
  type: "text",
@@ -1262,13 +1294,13 @@ function registerAllTools(server) {
1262
1294
  : `"${tool_id}" was not in your favorites.`,
1263
1295
  },
1264
1296
  ],
1265
- };
1297
+ });
1266
1298
  }
1267
1299
  catch (err) {
1268
- return {
1300
+ return appendUpdateNotice({
1269
1301
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1270
1302
  isError: true,
1271
- };
1303
+ });
1272
1304
  }
1273
1305
  });
1274
1306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caravo/mcp",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "The API marketplace built for autonomous AI agents. Search, execute, and pay for 200+ tools at $0.001–0.05 per call.",
5
5
  "type": "module",
6
6
  "bin": {