@caravo/mcp 0.1.26 → 0.1.28

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 +126 -85
  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,6 +256,35 @@ function stripDangerousFields(input) {
246
256
  }
247
257
  return cleaned;
248
258
  }
259
+ /**
260
+ * Resolve local file paths in tool input to base64.
261
+ * Detects file:// URIs and absolute paths with image extensions.
262
+ * Runs locally so base64 never enters the LLM context.
263
+ */
264
+ function resolveLocalFiles(input) {
265
+ const result = { ...input };
266
+ for (const [key, value] of Object.entries(result)) {
267
+ if (typeof value !== "string")
268
+ continue;
269
+ const filePath = toLocalPath(value);
270
+ if (!filePath)
271
+ continue;
272
+ if (!existsSync(filePath)) {
273
+ throw new Error(`Local file not found: ${filePath}`);
274
+ }
275
+ result[key] = readFileSync(filePath).toString("base64");
276
+ }
277
+ return result;
278
+ }
279
+ function toLocalPath(value) {
280
+ if (value.startsWith("file:///"))
281
+ return value.slice(7);
282
+ if (value.startsWith("file://"))
283
+ return value.slice(7);
284
+ if (/^\//.test(value) && /\.(png|jpe?g|gif|webp|bmp|svg|tiff?)$/i.test(value))
285
+ return value;
286
+ return null;
287
+ }
249
288
  // ─── Favorites registration ────────────────────────────────────────────────────
250
289
  // Track registered fav tool handles for dynamic add/remove
251
290
  const registeredFavTools = new Map();
@@ -342,9 +381,10 @@ function buildPostExecPrompt(execId, toolId) {
342
381
  function makeFavToolHandler(tool) {
343
382
  return async (args) => {
344
383
  // Extract dry_run before passing remaining args to the API
345
- const { dry_run, ...toolInput } = args;
384
+ const { dry_run, ...rawInput } = args;
385
+ const toolInput = resolveLocalFiles(rawInput);
346
386
  if (dry_run) {
347
- return dryRunProbe(tool.id, toolInput);
387
+ return appendUpdateNotice(await dryRunProbe(tool.id, toolInput));
348
388
  }
349
389
  try {
350
390
  const result = await apiPost(`/api/tools/${tool.id}/execute`, toolInput);
@@ -357,26 +397,26 @@ function makeFavToolHandler(tool) {
357
397
  ...formatOutput(result.output),
358
398
  ...reviewLines,
359
399
  ];
360
- return {
400
+ return appendUpdateNotice({
361
401
  content: [{ type: "text", text: lines.join("\n") }],
362
- };
402
+ });
363
403
  }
364
404
  if (result.x402Version || result.accepts) {
365
405
  const price = `$${tool.pricing.price_per_call}`;
366
- return {
406
+ return appendUpdateNotice({
367
407
  content: [{ type: "text", text: buildPaymentRequiredMessage(price) }],
368
408
  isError: true,
369
- };
409
+ });
370
410
  }
371
- return {
411
+ return appendUpdateNotice({
372
412
  content: [
373
413
  { type: "text", text: `Error: ${safeJsonText(result, false)}` },
374
414
  ],
375
415
  isError: true,
376
- };
416
+ });
377
417
  }
378
418
  catch (err) {
379
- return {
419
+ return appendUpdateNotice({
380
420
  content: [
381
421
  {
382
422
  type: "text",
@@ -384,7 +424,7 @@ function makeFavToolHandler(tool) {
384
424
  },
385
425
  ],
386
426
  isError: true,
387
- };
427
+ });
388
428
  }
389
429
  };
390
430
  }
@@ -525,19 +565,15 @@ function registerAllTools(server) {
525
565
  params.set("per_page", String(per_page));
526
566
  params.set("view", "agent");
527
567
  const data = await apiGet(`/api/tools?${params}`);
528
- let text = safeJsonText(data);
529
- if (pendingUpdate) {
530
- text += `\n\n[Update available: @caravo/mcp ${pendingUpdate.current} → ${pendingUpdate.latest}. Will auto-update on next MCP restart.]`;
531
- }
532
- return {
533
- content: [{ type: "text", text }],
534
- };
568
+ return appendUpdateNotice({
569
+ content: [{ type: "text", text: safeJsonText(data) }],
570
+ });
535
571
  }
536
572
  catch (err) {
537
- return {
573
+ return appendUpdateNotice({
538
574
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
539
575
  isError: true,
540
- };
576
+ });
541
577
  }
542
578
  });
543
579
  // ── Get tool info ────────────────────────────────────────────────────────────
@@ -556,21 +592,22 @@ function registerAllTools(server) {
556
592
  }
557
593
  try {
558
594
  const data = await apiGet(`/api/tools/${tool_id.trim()}`);
559
- return {
595
+ return appendUpdateNotice({
560
596
  content: [{ type: "text", text: safeJsonText(data) }],
561
- };
597
+ });
562
598
  }
563
599
  catch (err) {
564
- return {
600
+ return appendUpdateNotice({
565
601
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
566
602
  isError: true,
567
- };
603
+ });
568
604
  }
569
605
  });
570
606
  // ── use_tool (meta-tool) ─────────────────────────────────────────────────────
571
607
  server.registerTool("use_tool", {
572
608
  description: "Execute any marketplace tool by ID. Use get_tool_info first to see the required input schema. " +
573
609
  "Paid tools auto-pay via x402 (wallet) or API key balance. " +
610
+ "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. " +
574
611
  "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.",
575
612
  inputSchema: {
576
613
  tool_id: z.string().describe("The tool ID or slug to execute (e.g., 'black-forest-labs/flux.1-schnell' or 'alice/imagen-4')"),
@@ -582,15 +619,15 @@ function registerAllTools(server) {
582
619
  }, async ({ tool_id, input, dry_run }) => {
583
620
  const validationError = validateToolId(tool_id);
584
621
  if (validationError) {
585
- return {
622
+ return appendUpdateNotice({
586
623
  content: [{ type: "text", text: `Error: ${validationError}` }],
587
624
  isError: true,
588
- };
625
+ });
589
626
  }
590
- const cleanInput = stripDangerousFields(input);
627
+ const cleanInput = resolveLocalFiles(stripDangerousFields(input));
591
628
  // Dry-run mode: probe cost without executing or paying
592
629
  if (dry_run) {
593
- return dryRunProbe(tool_id.trim(), cleanInput);
630
+ return appendUpdateNotice(await dryRunProbe(tool_id.trim(), cleanInput));
594
631
  }
595
632
  try {
596
633
  const result = await apiPost(`/api/tools/${tool_id.trim()}/execute`, cleanInput);
@@ -603,28 +640,28 @@ function registerAllTools(server) {
603
640
  ...formatOutput(result.output),
604
641
  ...reviewLines,
605
642
  ];
606
- return {
643
+ return appendUpdateNotice({
607
644
  content: [{ type: "text", text: lines.join("\n") }],
608
- };
645
+ });
609
646
  }
610
647
  if (result.x402Version || result.accepts) {
611
648
  const price = result.accepts?.[0]?.amount
612
649
  ? `$${(parseInt(result.accepts[0].amount) / 1e6).toFixed(6)}`
613
650
  : "?";
614
- return {
651
+ return appendUpdateNotice({
615
652
  content: [{ type: "text", text: buildPaymentRequiredMessage(price) }],
616
653
  isError: true,
617
- };
654
+ });
618
655
  }
619
- return {
656
+ return appendUpdateNotice({
620
657
  content: [
621
658
  { type: "text", text: `Error: ${safeJsonText(result, false)}` },
622
659
  ],
623
660
  isError: true,
624
- };
661
+ });
625
662
  }
626
663
  catch (err) {
627
- return {
664
+ return appendUpdateNotice({
628
665
  content: [
629
666
  {
630
667
  type: "text",
@@ -632,7 +669,7 @@ function registerAllTools(server) {
632
669
  },
633
670
  ],
634
671
  isError: true,
635
- };
672
+ });
636
673
  }
637
674
  });
638
675
  // ── Submit review / upvote ───────────────────────────────────────────────────
@@ -686,12 +723,12 @@ function registerAllTools(server) {
686
723
  lines.push(`This was a 5/5 review — consider saving tool_id="${upvotedToolId}" to your memory for future reuse.`);
687
724
  }
688
725
  }
689
- return { content: [{ type: "text", text: lines.join("\n") }] };
726
+ return appendUpdateNotice({ content: [{ type: "text", text: lines.join("\n") }] });
690
727
  }
691
- return {
728
+ return appendUpdateNotice({
692
729
  content: [{ type: "text", text: result.error ? `Error: ${result.error}` : safeJsonText(result) }],
693
730
  isError: true,
694
- };
731
+ });
695
732
  }
696
733
  // New review mode
697
734
  if (rating == null || !comment) {
@@ -718,10 +755,10 @@ function registerAllTools(server) {
718
755
  agent_id,
719
756
  });
720
757
  if (result.error) {
721
- return {
758
+ return appendUpdateNotice({
722
759
  content: [{ type: "text", text: `Error: ${result.error}` }],
723
760
  isError: true,
724
- };
761
+ });
725
762
  }
726
763
  // The API returns the review record with tool_id derived from execution
727
764
  const reviewToolId = result.tool_id || "unknown";
@@ -736,13 +773,13 @@ function registerAllTools(server) {
736
773
  lines.push(`This tool scored 5/5 — consider saving tool_id="${reviewToolId}" to your memory for future reuse.`);
737
774
  }
738
775
  }
739
- return { content: [{ type: "text", text: lines.join("\n") }] };
776
+ return appendUpdateNotice({ content: [{ type: "text", text: lines.join("\n") }] });
740
777
  }
741
778
  catch (err) {
742
- return {
779
+ return appendUpdateNotice({
743
780
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
744
781
  isError: true,
745
- };
782
+ });
746
783
  }
747
784
  });
748
785
  // ── Wallet info ──────────────────────────────────────────────────────────────
@@ -804,14 +841,14 @@ function registerAllTools(server) {
804
841
  else {
805
842
  info.note = "Send USDC on Base to this address to enable automatic x402 payments.";
806
843
  }
807
- return {
844
+ return appendUpdateNotice({
808
845
  content: [
809
846
  {
810
847
  type: "text",
811
848
  text: JSON.stringify(info, null, 2),
812
849
  },
813
850
  ],
814
- };
851
+ });
815
852
  });
816
853
  // ── Login (browser-based account connect) ────────────────────────────────────
817
854
  server.registerTool("login", {
@@ -893,7 +930,9 @@ function registerAllTools(server) {
893
930
  "Removes the saved API key and unregisters all favorited tools from this session.",
894
931
  inputSchema: {},
895
932
  }, async () => {
896
- if (!API_KEY) {
933
+ // Check both in-memory key and config file (key may have been set by CLI login after MCP started)
934
+ const configKey = loadConfig().api_key;
935
+ if (!API_KEY && !configKey) {
897
936
  return {
898
937
  content: [{ type: "text", text: "Not logged in — already using x402 wallet payments." }],
899
938
  };
@@ -902,9 +941,11 @@ function registerAllTools(server) {
902
941
  API_KEY = undefined;
903
942
  // 2. Remove key from config file
904
943
  try {
905
- const config = loadConfig();
906
- delete config.api_key;
907
- saveConfig(config);
944
+ if (configKey) {
945
+ const config = loadConfig();
946
+ delete config.api_key;
947
+ saveConfig(config);
948
+ }
908
949
  }
909
950
  catch {
910
951
  // config file may not exist — that's fine
@@ -939,15 +980,15 @@ function registerAllTools(server) {
939
980
  }, async () => {
940
981
  try {
941
982
  const data = await apiGet("/api/tags");
942
- return {
983
+ return appendUpdateNotice({
943
984
  content: [{ type: "text", text: safeJsonText(data) }],
944
- };
985
+ });
945
986
  }
946
987
  catch (err) {
947
- return {
988
+ return appendUpdateNotice({
948
989
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
949
990
  isError: true,
950
- };
991
+ });
951
992
  }
952
993
  });
953
994
  // ── List providers ───────────────────────────────────────────────────────────
@@ -957,15 +998,15 @@ function registerAllTools(server) {
957
998
  }, async () => {
958
999
  try {
959
1000
  const data = await apiGet("/api/providers");
960
- return {
1001
+ return appendUpdateNotice({
961
1002
  content: [{ type: "text", text: safeJsonText(data) }],
962
- };
1003
+ });
963
1004
  }
964
1005
  catch (err) {
965
- return {
1006
+ return appendUpdateNotice({
966
1007
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
967
1008
  isError: true,
968
- };
1009
+ });
969
1010
  }
970
1011
  });
971
1012
  // ── Tool Requests ───────────────────────────────────────────────────────────
@@ -992,15 +1033,15 @@ function registerAllTools(server) {
992
1033
  params.set("page", String(page));
993
1034
  params.set("per_page", String(per_page));
994
1035
  const data = await apiGet(`/api/tool-requests?${params}`);
995
- return {
1036
+ return appendUpdateNotice({
996
1037
  content: [{ type: "text", text: safeJsonText(data) }],
997
- };
1038
+ });
998
1039
  }
999
1040
  catch (err) {
1000
- return {
1041
+ return appendUpdateNotice({
1001
1042
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1002
1043
  isError: true,
1003
- };
1044
+ });
1004
1045
  }
1005
1046
  });
1006
1047
  server.registerTool("request_tool", {
@@ -1024,12 +1065,12 @@ function registerAllTools(server) {
1024
1065
  agent_id,
1025
1066
  });
1026
1067
  if (result.error) {
1027
- return {
1068
+ return appendUpdateNotice({
1028
1069
  content: [{ type: "text", text: `Error: ${result.error}` }],
1029
1070
  isError: true,
1030
- };
1071
+ });
1031
1072
  }
1032
- return {
1073
+ return appendUpdateNotice({
1033
1074
  content: [
1034
1075
  {
1035
1076
  type: "text",
@@ -1042,13 +1083,13 @@ function registerAllTools(server) {
1042
1083
  ].join("\n"),
1043
1084
  },
1044
1085
  ],
1045
- };
1086
+ });
1046
1087
  }
1047
1088
  catch (err) {
1048
- return {
1089
+ return appendUpdateNotice({
1049
1090
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1050
1091
  isError: true,
1051
- };
1092
+ });
1052
1093
  }
1053
1094
  });
1054
1095
  server.registerTool("upvote_tool_request", {
@@ -1064,21 +1105,21 @@ function registerAllTools(server) {
1064
1105
  execution_id,
1065
1106
  });
1066
1107
  if (result.error) {
1067
- return {
1108
+ return appendUpdateNotice({
1068
1109
  content: [{ type: "text", text: `Error: ${result.error}` }],
1069
1110
  isError: true,
1070
- };
1111
+ });
1071
1112
  }
1072
1113
  const action = result.action === "already_upvoted" ? "Already upvoted" : "Upvoted";
1073
- return {
1114
+ return appendUpdateNotice({
1074
1115
  content: [{ type: "text", text: `✓ ${action} tool request ${request_id}` }],
1075
- };
1116
+ });
1076
1117
  }
1077
1118
  catch (err) {
1078
- return {
1119
+ return appendUpdateNotice({
1079
1120
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1080
1121
  isError: true,
1081
- };
1122
+ });
1082
1123
  }
1083
1124
  });
1084
1125
  // ── Favorites management ─────────────────────────────────────────────────────
@@ -1106,7 +1147,7 @@ function registerAllTools(server) {
1106
1147
  };
1107
1148
  }
1108
1149
  const tools = result.data ?? [];
1109
- return {
1150
+ return appendUpdateNotice({
1110
1151
  content: [
1111
1152
  {
1112
1153
  type: "text",
@@ -1122,13 +1163,13 @@ function registerAllTools(server) {
1122
1163
  }),
1123
1164
  },
1124
1165
  ],
1125
- };
1166
+ });
1126
1167
  }
1127
1168
  catch (err) {
1128
- return {
1169
+ return appendUpdateNotice({
1129
1170
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1130
1171
  isError: true,
1131
- };
1172
+ });
1132
1173
  }
1133
1174
  });
1134
1175
  server.registerTool("favorite_tool", {
@@ -1165,7 +1206,7 @@ function registerAllTools(server) {
1165
1206
  if (tool) {
1166
1207
  registerFavTool(server, tool);
1167
1208
  }
1168
- return {
1209
+ return appendUpdateNotice({
1169
1210
  content: [
1170
1211
  {
1171
1212
  type: "text",
@@ -1177,13 +1218,13 @@ function registerAllTools(server) {
1177
1218
  ].join("\n"),
1178
1219
  },
1179
1220
  ],
1180
- };
1221
+ });
1181
1222
  }
1182
1223
  catch (err) {
1183
- return {
1224
+ return appendUpdateNotice({
1184
1225
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1185
1226
  isError: true,
1186
- };
1227
+ });
1187
1228
  }
1188
1229
  });
1189
1230
  server.registerTool("unfavorite_tool", {
@@ -1218,7 +1259,7 @@ function registerAllTools(server) {
1218
1259
  registered.remove();
1219
1260
  registeredFavTools.delete(tool_id);
1220
1261
  }
1221
- return {
1262
+ return appendUpdateNotice({
1222
1263
  content: [
1223
1264
  {
1224
1265
  type: "text",
@@ -1227,13 +1268,13 @@ function registerAllTools(server) {
1227
1268
  : `"${tool_id}" was not in your favorites.`,
1228
1269
  },
1229
1270
  ],
1230
- };
1271
+ });
1231
1272
  }
1232
1273
  catch (err) {
1233
- return {
1274
+ return appendUpdateNotice({
1234
1275
  content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1235
1276
  isError: true,
1236
- };
1277
+ });
1237
1278
  }
1238
1279
  });
1239
1280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caravo/mcp",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
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": {