@caravo/mcp 0.1.22 → 0.1.23
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 +94 -6
- package/dist/version-check.js +100 -0
- package/package.json +1 -1
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`,
|
|
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:
|
|
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
|
|
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) {
|
|
@@ -1022,7 +1110,7 @@ function registerAllTools(server) {
|
|
|
1022
1110
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
1023
1111
|
const server = new McpServer({
|
|
1024
1112
|
name: "caravo",
|
|
1025
|
-
version:
|
|
1113
|
+
version: VERSION,
|
|
1026
1114
|
description: "The API marketplace built for autonomous AI agents. Search, execute, and pay for 200+ tools at $0.001–0.05 per call.",
|
|
1027
1115
|
icons: [
|
|
1028
1116
|
{
|
|
@@ -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