@caravo/mcp 0.1.22 → 0.1.24

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/dist/index.js CHANGED
@@ -22,7 +22,19 @@ import { homedir } from "os";
22
22
  import { join } from "path";
23
23
  import { loadOrCreateWallet } from "./wallet.js";
24
24
  import { fetchWithX402 } from "./x402.js";
25
+ import { checkForUpdate } from "./version-check.js";
26
+ import { createRequire } from "module";
27
+ const require = createRequire(import.meta.url);
28
+ const { version: VERSION } = require("../package.json");
25
29
  const API_BASE = process.env.CARAVO_URL ?? "https://www.caravo.ai";
30
+ // Auto-update: check for newer version on startup (async, non-blocking)
31
+ let pendingUpdate = null;
32
+ checkForUpdate("@caravo/mcp", VERSION).then((info) => {
33
+ if (info) {
34
+ pendingUpdate = info;
35
+ process.stderr.write(`[caravo] update available: ${info.current} → ${info.latest} (will auto-update on next restart)\n`);
36
+ }
37
+ });
26
38
  // ─── Shared description blocks (used in both `instructions` and `search_tools`) ─
27
39
  const ABOUT_CARAVO = "Caravo (https://caravo.ai) — the API marketplace for AI agents. 200+ tools at $0.001–$0.05/call.\n" +
28
40
  "Docs & source: https://github.com/Caravo-AI/Caravo-MCP";
@@ -313,8 +325,13 @@ function buildPostExecPrompt(execId, toolId) {
313
325
  }
314
326
  function makeFavToolHandler(tool) {
315
327
  return async (args) => {
328
+ // Extract dry_run before passing remaining args to the API
329
+ const { dry_run, ...toolInput } = args;
330
+ if (dry_run) {
331
+ return dryRunProbe(tool.id, toolInput);
332
+ }
316
333
  try {
317
- const result = await apiPost(`/api/tools/${tool.id}/execute`, args);
334
+ const result = await apiPost(`/api/tools/${tool.id}/execute`, toolInput);
318
335
  if (result.success) {
319
336
  const execId = result.execution_id || null;
320
337
  const reviewLines = buildPostExecPrompt(execId, tool.id);
@@ -361,10 +378,12 @@ function registerFavTool(server, tool) {
361
378
  const priceLabel = tool.pricing.price_per_call > 0
362
379
  ? `$${tool.pricing.price_per_call}/call`
363
380
  : "Free";
381
+ const schema = buildSchemaShape(tool);
382
+ schema.dry_run = z.boolean().optional().describe("Preview cost without executing");
364
383
  const registered = server.registerTool(`fav:${tool.id}`, {
365
384
  title: `★ ${tool.name}`,
366
385
  description: `[${tool.provider}] ${tool.description} | ${priceLabel} | Tags: ${tool.tags.join(", ")}`,
367
- inputSchema: buildSchemaShape(tool),
386
+ inputSchema: schema,
368
387
  }, makeFavToolHandler(tool));
369
388
  registeredFavTools.set(tool.id, registered);
370
389
  }
@@ -389,6 +408,63 @@ async function loadFavoriteTools(server) {
389
408
  process.stderr.write(`[caravo] warning: could not load favorites: ${e}\n`);
390
409
  }
391
410
  }
411
+ // ─── Dry-run helper ─────────────────────────────────────────────────────────
412
+ async function dryRunProbe(toolId, input) {
413
+ try {
414
+ // Send a plain POST with no auth/payment headers to trigger a 402 for paid tools
415
+ const url = `${API_BASE}/api/tools/${toolId}/execute`;
416
+ const resp = await fetch(url, {
417
+ method: "POST",
418
+ headers: { "Content-Type": "application/json" },
419
+ body: JSON.stringify(input),
420
+ });
421
+ if (resp.status === 402) {
422
+ // Parse cost from 402 response
423
+ let cost = "unknown";
424
+ try {
425
+ const body = await resp.json();
426
+ const amount = body?.accepts?.[0]?.maxAmountRequired ?? body?.accepts?.[0]?.amount;
427
+ if (amount) {
428
+ cost = `$${(parseInt(amount) / 1e6).toFixed(6)}`;
429
+ }
430
+ }
431
+ catch {
432
+ // Header fallback
433
+ const header = resp.headers.get("payment-required");
434
+ if (header) {
435
+ try {
436
+ const pr = JSON.parse(atob(header));
437
+ const amount = pr?.accepts?.[0]?.maxAmountRequired ?? pr?.accepts?.[0]?.amount;
438
+ if (amount)
439
+ cost = `$${(parseInt(amount) / 1e6).toFixed(6)}`;
440
+ }
441
+ catch { /* ignore */ }
442
+ }
443
+ }
444
+ return {
445
+ content: [{ type: "text", text: `Preview: ${toolId} costs ${cost} per call (no payment was made)` }],
446
+ };
447
+ }
448
+ if (resp.ok) {
449
+ return {
450
+ content: [{ type: "text", text: `Preview: ${toolId} is free ($0.00 per call)` }],
451
+ };
452
+ }
453
+ // Other error (e.g. 400 bad input)
454
+ const body = await resp.json().catch(() => ({}));
455
+ const errorMsg = body?.error ?? `HTTP ${resp.status}`;
456
+ return {
457
+ content: [{ type: "text", text: `Dry-run failed: ${errorMsg}` }],
458
+ isError: true,
459
+ };
460
+ }
461
+ catch (err) {
462
+ return {
463
+ content: [{ type: "text", text: `Dry-run error: ${err instanceof Error ? err.message : String(err)}` }],
464
+ isError: true,
465
+ };
466
+ }
467
+ }
392
468
  // ─── Static management + meta tools ───────────────────────────────────────────
393
469
  function registerAllTools(server) {
394
470
  // ── Core workflow tools (registered first for visibility) ──────────────────
@@ -405,10 +481,11 @@ function registerAllTools(server) {
405
481
  query: z.string().optional().describe("Search query"),
406
482
  tag: z.string().optional().describe("Filter by tag (name or slug)"),
407
483
  provider: z.string().optional().describe("Filter by provider slug"),
484
+ pricing_type: z.enum(["free", "paid"]).optional().describe("Filter by pricing: 'free' or 'paid'"),
408
485
  page: z.number().optional().describe("Page number (default 1)"),
409
486
  per_page: z.number().optional().describe("Results per page (default 10)"),
410
487
  },
411
- }, async ({ query, tag, provider, page = 1, per_page = 10 }) => {
488
+ }, async ({ query, tag, provider, pricing_type, page = 1, per_page = 10 }) => {
412
489
  if (!Number.isInteger(page) || page < 1) {
413
490
  return { content: [{ type: "text", text: "Error: page must be a positive integer" }], isError: true };
414
491
  }
@@ -425,12 +502,18 @@ function registerAllTools(server) {
425
502
  params.set("tag", tag);
426
503
  if (provider)
427
504
  params.set("provider", provider);
505
+ if (pricing_type)
506
+ params.set("pricing_type", pricing_type);
428
507
  params.set("page", String(page));
429
508
  params.set("per_page", String(per_page));
430
509
  params.set("view", "agent");
431
510
  const data = await apiGet(`/api/tools?${params}`);
511
+ let text = JSON.stringify(data, null, 2);
512
+ if (pendingUpdate) {
513
+ text += `\n\n[Update available: @caravo/mcp ${pendingUpdate.current} → ${pendingUpdate.latest}. Will auto-update on next MCP restart.]`;
514
+ }
432
515
  return {
433
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
516
+ content: [{ type: "text", text }],
434
517
  };
435
518
  });
436
519
  // ── Get tool info ────────────────────────────────────────────────────────────
@@ -462,8 +545,9 @@ function registerAllTools(server) {
462
545
  input: z
463
546
  .record(z.string(), z.unknown())
464
547
  .describe("Input parameters for the tool (see get_tool_info for schema)"),
548
+ dry_run: z.boolean().optional().describe("Preview execution cost without actually running the tool or making a payment"),
465
549
  },
466
- }, async ({ tool_id, input }) => {
550
+ }, async ({ tool_id, input, dry_run }) => {
467
551
  const validationError = validateToolId(tool_id);
468
552
  if (validationError) {
469
553
  return {
@@ -472,6 +556,10 @@ function registerAllTools(server) {
472
556
  };
473
557
  }
474
558
  const cleanInput = stripDangerousFields(input);
559
+ // Dry-run mode: probe cost without executing or paying
560
+ if (dry_run) {
561
+ return dryRunProbe(tool_id.trim(), cleanInput);
562
+ }
475
563
  try {
476
564
  const result = await apiPost(`/api/tools/${tool_id.trim()}/execute`, cleanInput);
477
565
  if (result.success) {
@@ -645,16 +733,42 @@ function registerAllTools(server) {
645
733
  catch {
646
734
  // ignore
647
735
  }
736
+ const info = {
737
+ address: wallet.address,
738
+ network: "Base mainnet (eip155:8453)",
739
+ usdc_balance: balance,
740
+ };
741
+ // Check if wallet is empty and build funding guidance
742
+ const isWalletEmpty = balance === "0.000000 USDC" || balance === "unknown (check manually)";
743
+ if (isWalletEmpty && !API_KEY) {
744
+ info.funding_guide = [
745
+ "Your wallet has no USDC. Two options to get started:",
746
+ "",
747
+ "Option A — Caravo balance (recommended):",
748
+ ' Run the "login" tool to connect your account, then top up at https://www.caravo.ai/dashboard',
749
+ "",
750
+ "Option B — USDC wallet (no registration):",
751
+ ` Send USDC on Base to ${wallet.address}`,
752
+ "",
753
+ "Top up once to unlock 200+ tools at $0.001–$0.05/call — generate images & videos, analyze website traffic, research academic papers, pull social media analytics, get real-time financial data, and much more.",
754
+ ].join("\n");
755
+ }
756
+ else if (isWalletEmpty && API_KEY) {
757
+ info.funding_guide = [
758
+ "Your wallet has no USDC.",
759
+ "",
760
+ "Top up your Caravo balance at https://www.caravo.ai/dashboard",
761
+ `Or send USDC on Base to ${wallet.address}`,
762
+ ].join("\n");
763
+ }
764
+ else {
765
+ info.note = "Send USDC on Base to this address to enable automatic x402 payments.";
766
+ }
648
767
  return {
649
768
  content: [
650
769
  {
651
770
  type: "text",
652
- text: JSON.stringify({
653
- address: wallet.address,
654
- network: "Base mainnet (eip155:8453)",
655
- usdc_balance: balance,
656
- note: "Send USDC on Base to this address to enable automatic x402 payments.",
657
- }, null, 2),
771
+ text: JSON.stringify(info, null, 2),
658
772
  },
659
773
  ],
660
774
  };
@@ -1022,7 +1136,7 @@ function registerAllTools(server) {
1022
1136
  // ─── Main ─────────────────────────────────────────────────────────────────────
1023
1137
  const server = new McpServer({
1024
1138
  name: "caravo",
1025
- version: "0.1.15",
1139
+ version: VERSION,
1026
1140
  description: "The API marketplace built for autonomous AI agents. Search, execute, and pay for 200+ tools at $0.001–0.05 per call.",
1027
1141
  icons: [
1028
1142
  {
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Auto-update: checks npm for newer version, clears npx cache if outdated.
3
+ *
4
+ * Flow:
5
+ * 1. On startup, check npm registry (cached 24h in ~/.caravo/version-check.json)
6
+ * 2. If a newer version exists and we're running from npx cache, delete our cache entry
7
+ * 3. Next time the MCP host restarts the server, npx re-downloads the latest version
8
+ *
9
+ * All operations are non-fatal — errors are silently ignored.
10
+ */
11
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from "fs";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+ const CONFIG_DIR = join(homedir(), ".caravo");
15
+ const CACHE_FILE = join(CONFIG_DIR, "version-check.json");
16
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
17
+ function isNewer(latest, current) {
18
+ const l = latest.split(".").map(Number);
19
+ const c = current.split(".").map(Number);
20
+ for (let i = 0; i < 3; i++) {
21
+ if ((l[i] ?? 0) !== (c[i] ?? 0))
22
+ return (l[i] ?? 0) > (c[i] ?? 0);
23
+ }
24
+ return false;
25
+ }
26
+ function readCache() {
27
+ try {
28
+ if (existsSync(CACHE_FILE)) {
29
+ return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
30
+ }
31
+ }
32
+ catch { /* ignore */ }
33
+ return {};
34
+ }
35
+ function writeCache(cache) {
36
+ try {
37
+ mkdirSync(CONFIG_DIR, { recursive: true });
38
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
39
+ }
40
+ catch { /* ignore */ }
41
+ }
42
+ /**
43
+ * Clear our package from the npx cache (~/.npm/_npx/).
44
+ * This ensures the next `npx @caravo/mcp` invocation downloads the latest version.
45
+ */
46
+ function clearNpxCache(packageName) {
47
+ const npxDir = join(homedir(), ".npm", "_npx");
48
+ try {
49
+ if (!existsSync(npxDir))
50
+ return;
51
+ for (const entry of readdirSync(npxDir)) {
52
+ const pkgJsonPath = join(npxDir, entry, "node_modules", packageName, "package.json");
53
+ try {
54
+ if (existsSync(pkgJsonPath)) {
55
+ rmSync(join(npxDir, entry), { recursive: true, force: true });
56
+ }
57
+ }
58
+ catch { /* skip */ }
59
+ }
60
+ }
61
+ catch { /* ignore */ }
62
+ }
63
+ /**
64
+ * Check npm registry for a newer version.
65
+ * Returns UpdateInfo if an update is available, null otherwise.
66
+ * Automatically clears npx cache when outdated.
67
+ */
68
+ export async function checkForUpdate(packageName, currentVersion) {
69
+ try {
70
+ const cache = readCache();
71
+ const cached = cache[packageName];
72
+ const now = Date.now();
73
+ // Use cache if fresh
74
+ if (cached && now - cached.checkedAt < CHECK_INTERVAL_MS) {
75
+ return isNewer(cached.latest, currentVersion)
76
+ ? { current: currentVersion, latest: cached.latest }
77
+ : null;
78
+ }
79
+ // Fetch from npm registry
80
+ const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
81
+ signal: AbortSignal.timeout(3000),
82
+ });
83
+ if (!resp.ok)
84
+ return null;
85
+ const data = (await resp.json());
86
+ const latest = data.version;
87
+ // Update cache
88
+ cache[packageName] = { latest, checkedAt: now };
89
+ writeCache(cache);
90
+ if (isNewer(latest, currentVersion)) {
91
+ // Clear npx cache so next restart gets the new version
92
+ clearNpxCache(packageName);
93
+ return { current: currentVersion, latest };
94
+ }
95
+ return null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caravo/mcp",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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": {