@agentwonderland/mcp 0.1.3 → 0.1.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.
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +14 -1
- package/dist/core/file-upload.d.ts +9 -0
- package/dist/core/file-upload.js +101 -0
- package/dist/core/formatters.js +5 -8
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/tools/run.js +81 -26
- package/dist/tools/solve.js +69 -15
- package/package.json +1 -1
- package/src/core/config.ts +20 -1
- package/src/core/file-upload.ts +114 -0
- package/src/core/formatters.ts +5 -10
- package/src/core/index.ts +1 -0
- package/src/tools/run.ts +93 -27
- package/src/tools/solve.ts +70 -15
package/dist/core/config.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface Config {
|
|
|
21
21
|
defaultWallet: string | null;
|
|
22
22
|
card: CardConfig | null;
|
|
23
23
|
favorites: string[];
|
|
24
|
+
/** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
|
|
25
|
+
confirmBeforeSpend: boolean;
|
|
26
|
+
/** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
|
|
27
|
+
defaultTipAmount: number;
|
|
24
28
|
}
|
|
25
29
|
/** All supported chain identifiers. */
|
|
26
30
|
export declare const SUPPORTED_CHAINS: readonly ["tempo", "base", "solana"];
|
|
@@ -35,6 +39,8 @@ export declare function saveConfig(updates: Partial<Config>): void;
|
|
|
35
39
|
export declare function getApiUrl(): string;
|
|
36
40
|
export declare function getApiKey(): string | null;
|
|
37
41
|
export declare function isAuthenticated(): boolean;
|
|
42
|
+
export declare function requiresSpendConfirmation(): boolean;
|
|
43
|
+
export declare function getDefaultTipAmount(): number;
|
|
38
44
|
/**
|
|
39
45
|
* Get all wallets from config + env var synthetic wallets.
|
|
40
46
|
*/
|
package/dist/core/config.js
CHANGED
|
@@ -27,6 +27,7 @@ function ensureConfigDir() {
|
|
|
27
27
|
function migrateIfNeeded(raw) {
|
|
28
28
|
// If wallets array already exists, treat as new format
|
|
29
29
|
if (Array.isArray(raw.wallets)) {
|
|
30
|
+
const r = raw;
|
|
30
31
|
return {
|
|
31
32
|
apiUrl: raw.apiUrl ?? DEFAULT_API_URL,
|
|
32
33
|
apiKey: raw.apiKey ?? null,
|
|
@@ -34,7 +35,9 @@ function migrateIfNeeded(raw) {
|
|
|
34
35
|
wallets: raw.wallets,
|
|
35
36
|
defaultWallet: raw.defaultWallet ?? null,
|
|
36
37
|
card: raw.card ?? null,
|
|
37
|
-
favorites:
|
|
38
|
+
favorites: r.favorites ?? [],
|
|
39
|
+
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
40
|
+
defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
// Build wallets from legacy flat fields
|
|
@@ -102,6 +105,8 @@ function migrateIfNeeded(raw) {
|
|
|
102
105
|
defaultWallet,
|
|
103
106
|
card,
|
|
104
107
|
favorites: [],
|
|
108
|
+
confirmBeforeSpend: true,
|
|
109
|
+
defaultTipAmount: 0,
|
|
105
110
|
};
|
|
106
111
|
// Write migrated config (only if there was something to migrate)
|
|
107
112
|
if (raw.tempoPrivateKey || raw.evmPrivateKey || raw.stripeConsumerToken) {
|
|
@@ -120,6 +125,8 @@ export function getConfig() {
|
|
|
120
125
|
defaultWallet: null,
|
|
121
126
|
card: null,
|
|
122
127
|
favorites: [],
|
|
128
|
+
confirmBeforeSpend: true,
|
|
129
|
+
defaultTipAmount: 0,
|
|
123
130
|
};
|
|
124
131
|
if (!existsSync(CONFIG_FILE)) {
|
|
125
132
|
return defaults;
|
|
@@ -154,6 +161,12 @@ export function getApiKey() {
|
|
|
154
161
|
export function isAuthenticated() {
|
|
155
162
|
return getApiKey() !== null;
|
|
156
163
|
}
|
|
164
|
+
export function requiresSpendConfirmation() {
|
|
165
|
+
return getConfig().confirmBeforeSpend;
|
|
166
|
+
}
|
|
167
|
+
export function getDefaultTipAmount() {
|
|
168
|
+
return getConfig().defaultTipAmount;
|
|
169
|
+
}
|
|
157
170
|
// ── Wallet helpers ─────────────────────────────────────────────────
|
|
158
171
|
/**
|
|
159
172
|
* Get all wallets from config + env var synthetic wallets.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a string looks like a local file path (not a URL).
|
|
3
|
+
*/
|
|
4
|
+
export declare function isLocalFilePath(value: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Scan input object for local file paths, upload them to temporary storage,
|
|
7
|
+
* and replace with download URLs. Non-file values are left unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export declare function uploadLocalFiles(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { getApiUrl } from "./config.js";
|
|
5
|
+
const EXT_TO_MIME = {
|
|
6
|
+
pdf: "application/pdf",
|
|
7
|
+
png: "image/png",
|
|
8
|
+
jpg: "image/jpeg",
|
|
9
|
+
jpeg: "image/jpeg",
|
|
10
|
+
gif: "image/gif",
|
|
11
|
+
webp: "image/webp",
|
|
12
|
+
svg: "image/svg+xml",
|
|
13
|
+
mp3: "audio/mpeg",
|
|
14
|
+
mp4: "video/mp4",
|
|
15
|
+
wav: "audio/wav",
|
|
16
|
+
csv: "text/csv",
|
|
17
|
+
json: "application/json",
|
|
18
|
+
txt: "text/plain",
|
|
19
|
+
html: "text/html",
|
|
20
|
+
xml: "application/xml",
|
|
21
|
+
zip: "application/zip",
|
|
22
|
+
doc: "application/msword",
|
|
23
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
24
|
+
xls: "application/vnd.ms-excel",
|
|
25
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
26
|
+
};
|
|
27
|
+
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
|
|
28
|
+
/**
|
|
29
|
+
* Check if a string looks like a local file path (not a URL).
|
|
30
|
+
*/
|
|
31
|
+
export function isLocalFilePath(value) {
|
|
32
|
+
if (value.startsWith("http://") || value.startsWith("https://") || value.startsWith("data:")) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (value.includes("\n"))
|
|
36
|
+
return false;
|
|
37
|
+
// Unix absolute path, home dir path, or Windows drive path
|
|
38
|
+
return value.startsWith("/") || value.startsWith("~/") || /^[A-Za-z]:\\/.test(value);
|
|
39
|
+
}
|
|
40
|
+
function resolvePath(filePath) {
|
|
41
|
+
if (filePath.startsWith("~/")) {
|
|
42
|
+
return resolve(homedir(), filePath.slice(2));
|
|
43
|
+
}
|
|
44
|
+
return resolve(filePath);
|
|
45
|
+
}
|
|
46
|
+
function getMimeType(filename) {
|
|
47
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
48
|
+
return EXT_TO_MIME[ext] ?? "application/octet-stream";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Upload a local file to the gateway's temporary storage.
|
|
52
|
+
* Returns the presigned download URL.
|
|
53
|
+
*/
|
|
54
|
+
async function uploadFile(filePath) {
|
|
55
|
+
const resolved = resolvePath(filePath);
|
|
56
|
+
const buffer = readFileSync(resolved);
|
|
57
|
+
if (buffer.length > MAX_UPLOAD_SIZE) {
|
|
58
|
+
throw new Error(`File ${basename(resolved)} exceeds 50MB upload limit (${(buffer.length / (1024 * 1024)).toFixed(1)}MB)`);
|
|
59
|
+
}
|
|
60
|
+
const filename = basename(resolved);
|
|
61
|
+
const contentType = getMimeType(filename);
|
|
62
|
+
const apiUrl = getApiUrl();
|
|
63
|
+
const res = await fetch(`${apiUrl}/uploads`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
file: buffer.toString("base64"),
|
|
68
|
+
filename,
|
|
69
|
+
content_type: contentType,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
const body = await res.json().catch(() => ({}));
|
|
74
|
+
throw new Error(`Upload failed: ${body.error ?? res.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
const result = await res.json();
|
|
77
|
+
return result.url;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Scan input object for local file paths, upload them to temporary storage,
|
|
81
|
+
* and replace with download URLs. Non-file values are left unchanged.
|
|
82
|
+
*/
|
|
83
|
+
export async function uploadLocalFiles(input) {
|
|
84
|
+
const result = { ...input };
|
|
85
|
+
for (const [key, value] of Object.entries(result)) {
|
|
86
|
+
if (typeof value !== "string")
|
|
87
|
+
continue;
|
|
88
|
+
if (!isLocalFilePath(value))
|
|
89
|
+
continue;
|
|
90
|
+
const resolved = resolvePath(value);
|
|
91
|
+
if (!existsSync(resolved))
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
result[key] = await uploadFile(value);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Leave original value if upload fails
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
package/dist/core/formatters.js
CHANGED
|
@@ -115,15 +115,12 @@ export function formatRunResult(result, opts) {
|
|
|
115
115
|
const costStr = cost != null ? `$${cost.toFixed(cost < 0.01 ? 6 : 2)}` : "";
|
|
116
116
|
const latency = result.latency_ms != null ? `${result.latency_ms}ms` : "";
|
|
117
117
|
const method = opts?.paymentMethod ?? "";
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
costStr ? `
|
|
121
|
-
|
|
122
|
-
latency ? `latency: ${latency}` : "",
|
|
123
|
-
].filter(Boolean);
|
|
124
|
-
lines.push(parts.join(" • "));
|
|
118
|
+
lines.push(`${status} ${agent}${latency ? ` (${latency})` : ""}`);
|
|
119
|
+
if (costStr) {
|
|
120
|
+
lines.push(`Paid: ${costStr}${method ? ` via ${method}` : ""}`);
|
|
121
|
+
}
|
|
125
122
|
if (result.job_id) {
|
|
126
|
-
lines.push(`
|
|
123
|
+
lines.push(`Job ID: ${result.job_id}`);
|
|
127
124
|
}
|
|
128
125
|
return lines.join("\n");
|
|
129
126
|
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/dist/tools/run.js
CHANGED
|
@@ -1,38 +1,61 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { apiGet, apiPostWithPayment } from "../core/api-client.js";
|
|
2
|
+
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
3
|
+
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
3
4
|
import { getConfiguredMethods, hasWalletConfigured } from "../core/payments.js";
|
|
5
|
+
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
4
6
|
import { formatRunResult } from "../core/formatters.js";
|
|
5
7
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
6
8
|
function text(t) {
|
|
7
9
|
return { content: [{ type: "text", text: t }] };
|
|
8
10
|
}
|
|
11
|
+
// Pending confirmations: agent_id → { agent, input, method }
|
|
12
|
+
const pendingRuns = new Map();
|
|
9
13
|
export function registerRunTools(server) {
|
|
10
|
-
server.tool("run_agent", "Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking.", {
|
|
11
|
-
agent_id: z.string().describe("Agent ID (UUID or
|
|
14
|
+
server.tool("run_agent", "Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking. If spending confirmation is enabled, first call returns a price quote — call again with confirmed: true to execute.", {
|
|
15
|
+
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
12
16
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
13
17
|
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
14
|
-
|
|
18
|
+
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
19
|
+
}, async ({ agent_id, input, pay_with, confirmed }) => {
|
|
15
20
|
if (!hasWalletConfigured()) {
|
|
16
21
|
return text("No wallet configured. Set one up first:\n\n" +
|
|
17
22
|
' wallet_setup({ action: "create", name: "my-wallet" })\n\n' +
|
|
18
23
|
"Then fund it with USDC on Tempo and try again.");
|
|
19
24
|
}
|
|
20
|
-
// Resolve
|
|
21
|
-
let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
// Resolve agent and fetch details
|
|
26
|
+
let agent;
|
|
27
|
+
try {
|
|
28
|
+
agent = await apiGet(`/agents/${agent_id}`);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
|
|
32
|
+
}
|
|
33
|
+
const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
|
|
34
|
+
const agentName = agent.name ?? agent_id;
|
|
35
|
+
// Confirmation step: show price and wait for confirmed: true
|
|
36
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
37
|
+
pendingRuns.set(agent.id, {
|
|
38
|
+
agent: { id: agent.id, name: agentName, price },
|
|
39
|
+
input,
|
|
40
|
+
method: pay_with,
|
|
41
|
+
});
|
|
42
|
+
return text([
|
|
43
|
+
`Ready to run ${agentName}`,
|
|
44
|
+
"",
|
|
45
|
+
` Cost: $${price.toFixed(2)}`,
|
|
46
|
+
` Payment: ${pay_with ?? getConfiguredMethods()[0] ?? "auto"}`,
|
|
47
|
+
"",
|
|
48
|
+
"To proceed, call:",
|
|
49
|
+
` run_agent({ agent_id: "${agent.id}", input: <same>, confirmed: true })`,
|
|
50
|
+
"",
|
|
51
|
+
"To cancel, do nothing.",
|
|
52
|
+
].join("\n"));
|
|
31
53
|
}
|
|
32
54
|
const method = pay_with;
|
|
55
|
+
const processedInput = await uploadLocalFiles(input);
|
|
33
56
|
let result;
|
|
34
57
|
try {
|
|
35
|
-
result = await apiPostWithPayment(`/agents/${
|
|
58
|
+
result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
|
|
36
59
|
}
|
|
37
60
|
catch (err) {
|
|
38
61
|
const apiErr = err;
|
|
@@ -47,32 +70,64 @@ export function registerRunTools(server) {
|
|
|
47
70
|
}
|
|
48
71
|
return text(`Error: ${msg}`);
|
|
49
72
|
}
|
|
73
|
+
// Clean up pending confirmation
|
|
74
|
+
pendingRuns.delete(agent.id);
|
|
50
75
|
const formatted = formatRunResult(result, {
|
|
51
76
|
paymentMethod: method ?? getConfiguredMethods()[0],
|
|
52
77
|
});
|
|
53
78
|
const jobId = result.job_id ?? "";
|
|
54
|
-
const
|
|
79
|
+
const resultAgentId = result.agent_id ?? agent.id;
|
|
55
80
|
if (result.feedback_token) {
|
|
56
|
-
storeFeedbackToken(jobId, result.feedback_token,
|
|
81
|
+
storeFeedbackToken(jobId, result.feedback_token, resultAgentId);
|
|
57
82
|
}
|
|
83
|
+
const actualCost = result.cost;
|
|
58
84
|
const status = result.status;
|
|
85
|
+
// Auto-tip if configured and run succeeded
|
|
86
|
+
const defaultTip = getDefaultTipAmount();
|
|
87
|
+
let tipLine = "";
|
|
88
|
+
if (status === "success" && defaultTip > 0 && result.feedback_token) {
|
|
89
|
+
try {
|
|
90
|
+
await apiPost("/tips", {
|
|
91
|
+
job_id: jobId,
|
|
92
|
+
agent_id: resultAgentId,
|
|
93
|
+
amount: defaultTip,
|
|
94
|
+
feedback_token: result.feedback_token,
|
|
95
|
+
});
|
|
96
|
+
tipLine = `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
tipLine = `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
59
102
|
let prompt;
|
|
60
103
|
if (status === "success") {
|
|
104
|
+
const costLine = actualCost != null ? `You paid $${actualCost.toFixed(actualCost < 0.01 ? 4 : 2)} for this run.` : "";
|
|
61
105
|
prompt = [
|
|
62
106
|
"",
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
107
|
+
"════════════════════════════════════════",
|
|
108
|
+
costLine,
|
|
109
|
+
tipLine,
|
|
110
|
+
"",
|
|
111
|
+
"Rate this result:",
|
|
112
|
+
` rate_agent({ job_id: "${jobId}", rating: 5, comment: "..." })`,
|
|
113
|
+
...(defaultTip <= 0 ? [
|
|
114
|
+
"",
|
|
115
|
+
"Tip the builder:",
|
|
116
|
+
` tip_agent({ job_id: "${jobId}", agent_id: "${resultAgentId}", amount: 0.25 })`,
|
|
117
|
+
] : []),
|
|
118
|
+
"",
|
|
119
|
+
"Save for later:",
|
|
120
|
+
` favorite_agent({ agent_id: "${resultAgentId}" })`,
|
|
121
|
+
"════════════════════════════════════════",
|
|
68
122
|
].join("\n");
|
|
69
123
|
}
|
|
70
124
|
else {
|
|
71
125
|
prompt = [
|
|
72
126
|
"",
|
|
73
|
-
"
|
|
74
|
-
"The agent execution failed.
|
|
75
|
-
"
|
|
127
|
+
"════════════════════════════════════════",
|
|
128
|
+
"The agent execution failed.",
|
|
129
|
+
"A refund has been initiated automatically.",
|
|
130
|
+
"════════════════════════════════════════",
|
|
76
131
|
].join("\n");
|
|
77
132
|
}
|
|
78
133
|
return text(formatted + prompt);
|
package/dist/tools/solve.js
CHANGED
|
@@ -1,23 +1,54 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
3
3
|
import { hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, } from "../core/payments.js";
|
|
4
|
+
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
4
5
|
import { agentList, formatRunResult } from "../core/formatters.js";
|
|
6
|
+
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
5
7
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
6
8
|
function text(t) {
|
|
7
9
|
return { content: [{ type: "text", text: t }] };
|
|
8
10
|
}
|
|
9
|
-
function
|
|
11
|
+
function feedbackPrompt(jobId, agentId, agentName, cost, tipSent) {
|
|
12
|
+
const defaultTip = getDefaultTipAmount();
|
|
13
|
+
const costLine = cost != null ? `You paid $${cost.toFixed(cost < 0.01 ? 4 : 2)} for this run.` : "";
|
|
10
14
|
return [
|
|
11
15
|
"",
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
16
|
+
"════════════════════════════════════════",
|
|
17
|
+
costLine,
|
|
18
|
+
tipSent ?? "",
|
|
19
|
+
"",
|
|
20
|
+
"Rate this result:",
|
|
21
|
+
` rate_agent({ job_id: "${jobId}", rating: 5, comment: "..." })`,
|
|
22
|
+
...(defaultTip <= 0 ? [
|
|
23
|
+
"",
|
|
24
|
+
"Tip the builder:",
|
|
25
|
+
` tip_agent({ job_id: "${jobId}", agent_id: "${agentId}", amount: 0.25 })`,
|
|
26
|
+
] : []),
|
|
27
|
+
"",
|
|
28
|
+
"Save for later:",
|
|
29
|
+
` favorite_agent({ agent_id: "${agentId}" })`,
|
|
30
|
+
"════════════════════════════════════════",
|
|
17
31
|
].join("\n");
|
|
18
32
|
}
|
|
33
|
+
async function autoTip(jobId, agentId, agentName, feedbackToken) {
|
|
34
|
+
const defaultTip = getDefaultTipAmount();
|
|
35
|
+
if (defaultTip <= 0)
|
|
36
|
+
return "";
|
|
37
|
+
try {
|
|
38
|
+
await apiPost("/tips", {
|
|
39
|
+
job_id: jobId,
|
|
40
|
+
agent_id: agentId,
|
|
41
|
+
amount: defaultTip,
|
|
42
|
+
feedback_token: feedbackToken,
|
|
43
|
+
});
|
|
44
|
+
return `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
19
50
|
export function registerSolveTools(server) {
|
|
20
|
-
server.tool("solve", "Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace
|
|
51
|
+
server.tool("solve", "Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace. If spending confirmation is enabled, returns a price quote first — call again with confirmed: true to execute.", {
|
|
21
52
|
intent: z
|
|
22
53
|
.string()
|
|
23
54
|
.describe("What you want to accomplish (natural language)"),
|
|
@@ -37,7 +68,11 @@ export function registerSolveTools(server) {
|
|
|
37
68
|
.string()
|
|
38
69
|
.optional()
|
|
39
70
|
.describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
40
|
-
|
|
71
|
+
confirmed: z
|
|
72
|
+
.boolean()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
75
|
+
}, async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
41
76
|
if (!hasWalletConfigured()) {
|
|
42
77
|
return text("No wallet configured. Set one up first:\n\n" +
|
|
43
78
|
' wallet_setup({ action: "create", name: "my-wallet" })\n\n' +
|
|
@@ -46,17 +81,21 @@ export function registerSolveTools(server) {
|
|
|
46
81
|
const method = pay_with ?? getConfiguredMethods()[0];
|
|
47
82
|
// Path 1: If authenticated, use the platform /solve route
|
|
48
83
|
try {
|
|
84
|
+
const processedInput = await uploadLocalFiles(input);
|
|
49
85
|
const result = await apiPost("/solve", {
|
|
50
86
|
intent,
|
|
51
|
-
input,
|
|
87
|
+
input: processedInput,
|
|
52
88
|
budget,
|
|
53
89
|
});
|
|
54
90
|
const jobId = result.job_id ?? "";
|
|
55
91
|
const agentId = result.agent_id ?? "";
|
|
92
|
+
const agentName = result.agent_name ?? "";
|
|
56
93
|
if (result.feedback_token) {
|
|
57
94
|
storeFeedbackToken(jobId, result.feedback_token, agentId);
|
|
58
95
|
}
|
|
59
|
-
|
|
96
|
+
const cost = result.cost;
|
|
97
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token) : "";
|
|
98
|
+
return text(formatRunResult(result) + feedbackPrompt(jobId, agentId, agentName, cost, tipMsg));
|
|
60
99
|
}
|
|
61
100
|
catch (err) {
|
|
62
101
|
const isAuthError = err instanceof Error &&
|
|
@@ -85,14 +124,29 @@ export function registerSolveTools(server) {
|
|
|
85
124
|
return cost <= budget;
|
|
86
125
|
});
|
|
87
126
|
const selected = affordable[0] ?? agents[0];
|
|
88
|
-
// Estimate cost for the selected agent
|
|
89
127
|
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
90
128
|
const estimatedCost = selected.pricingModel === "fixed"
|
|
91
129
|
? selectedPrice
|
|
92
130
|
: (inputTokens / 1000) * selectedPrice;
|
|
131
|
+
// Confirmation step: show discovery + price, wait for confirmed: true
|
|
132
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
133
|
+
return text([
|
|
134
|
+
discovery,
|
|
135
|
+
"",
|
|
136
|
+
`Best match: ${selected.name}`,
|
|
137
|
+
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
138
|
+
`Payment: ${method}`,
|
|
139
|
+
"",
|
|
140
|
+
"To proceed, call:",
|
|
141
|
+
` solve({ intent: "${intent}", input: <same>, budget: ${budget}, confirmed: true })`,
|
|
142
|
+
"",
|
|
143
|
+
"To cancel, do nothing.",
|
|
144
|
+
].join("\n"));
|
|
145
|
+
}
|
|
93
146
|
let result;
|
|
147
|
+
const processedInput2 = await uploadLocalFiles(input);
|
|
94
148
|
try {
|
|
95
|
-
result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input }, method);
|
|
149
|
+
result = await apiPostWithPayment(`/agents/${selected.id}/run`, { input: processedInput2 }, method);
|
|
96
150
|
}
|
|
97
151
|
catch (err) {
|
|
98
152
|
const apiErr = err;
|
|
@@ -103,13 +157,13 @@ export function registerSolveTools(server) {
|
|
|
103
157
|
}
|
|
104
158
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
105
159
|
}
|
|
106
|
-
// Cache feedback token if present
|
|
107
160
|
const jobId = result.job_id ?? "";
|
|
108
161
|
const agentId2 = result.agent_id ?? selected.id;
|
|
109
162
|
if (result.feedback_token) {
|
|
110
163
|
storeFeedbackToken(jobId, result.feedback_token, agentId2);
|
|
111
164
|
}
|
|
112
|
-
|
|
165
|
+
const actualCost = result.cost;
|
|
166
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId2, selected.name ?? "", result.feedback_token) : "";
|
|
113
167
|
const output = [
|
|
114
168
|
discovery,
|
|
115
169
|
"",
|
|
@@ -117,7 +171,7 @@ export function registerSolveTools(server) {
|
|
|
117
171
|
`Estimated cost: $${estimatedCost.toFixed(4)}`,
|
|
118
172
|
"",
|
|
119
173
|
formatRunResult(result, { paymentMethod: method }),
|
|
120
|
-
|
|
174
|
+
feedbackPrompt(jobId, agentId2, selected.name ?? "", actualCost, tipMsg),
|
|
121
175
|
].join("\n");
|
|
122
176
|
return text(output);
|
|
123
177
|
});
|
package/package.json
CHANGED
package/src/core/config.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface Config {
|
|
|
29
29
|
defaultWallet: string | null;
|
|
30
30
|
card: CardConfig | null;
|
|
31
31
|
favorites: string[];
|
|
32
|
+
/** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
|
|
33
|
+
confirmBeforeSpend: boolean;
|
|
34
|
+
/** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
|
|
35
|
+
defaultTipAmount: number;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/** All supported chain identifiers. */
|
|
@@ -84,6 +88,7 @@ interface LegacyConfig {
|
|
|
84
88
|
function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
85
89
|
// If wallets array already exists, treat as new format
|
|
86
90
|
if (Array.isArray(raw.wallets)) {
|
|
91
|
+
const r = raw as Record<string, unknown>;
|
|
87
92
|
return {
|
|
88
93
|
apiUrl: raw.apiUrl ?? DEFAULT_API_URL,
|
|
89
94
|
apiKey: raw.apiKey ?? null,
|
|
@@ -91,7 +96,9 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
91
96
|
wallets: raw.wallets,
|
|
92
97
|
defaultWallet: raw.defaultWallet ?? null,
|
|
93
98
|
card: raw.card ?? null,
|
|
94
|
-
favorites:
|
|
99
|
+
favorites: r.favorites as string[] ?? [],
|
|
100
|
+
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
101
|
+
defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
|
|
95
102
|
};
|
|
96
103
|
}
|
|
97
104
|
|
|
@@ -162,6 +169,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
162
169
|
defaultWallet,
|
|
163
170
|
card,
|
|
164
171
|
favorites: [],
|
|
172
|
+
confirmBeforeSpend: true,
|
|
173
|
+
defaultTipAmount: 0,
|
|
165
174
|
};
|
|
166
175
|
|
|
167
176
|
// Write migrated config (only if there was something to migrate)
|
|
@@ -184,6 +193,8 @@ export function getConfig(): Config {
|
|
|
184
193
|
defaultWallet: null,
|
|
185
194
|
card: null,
|
|
186
195
|
favorites: [],
|
|
196
|
+
confirmBeforeSpend: true,
|
|
197
|
+
defaultTipAmount: 0,
|
|
187
198
|
};
|
|
188
199
|
|
|
189
200
|
if (!existsSync(CONFIG_FILE)) {
|
|
@@ -222,6 +233,14 @@ export function isAuthenticated(): boolean {
|
|
|
222
233
|
return getApiKey() !== null;
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
export function requiresSpendConfirmation(): boolean {
|
|
237
|
+
return getConfig().confirmBeforeSpend;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getDefaultTipAmount(): number {
|
|
241
|
+
return getConfig().defaultTipAmount;
|
|
242
|
+
}
|
|
243
|
+
|
|
225
244
|
// ── Wallet helpers ─────────────────────────────────────────────────
|
|
226
245
|
|
|
227
246
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { getApiUrl } from "./config.js";
|
|
5
|
+
|
|
6
|
+
const EXT_TO_MIME: Record<string, string> = {
|
|
7
|
+
pdf: "application/pdf",
|
|
8
|
+
png: "image/png",
|
|
9
|
+
jpg: "image/jpeg",
|
|
10
|
+
jpeg: "image/jpeg",
|
|
11
|
+
gif: "image/gif",
|
|
12
|
+
webp: "image/webp",
|
|
13
|
+
svg: "image/svg+xml",
|
|
14
|
+
mp3: "audio/mpeg",
|
|
15
|
+
mp4: "video/mp4",
|
|
16
|
+
wav: "audio/wav",
|
|
17
|
+
csv: "text/csv",
|
|
18
|
+
json: "application/json",
|
|
19
|
+
txt: "text/plain",
|
|
20
|
+
html: "text/html",
|
|
21
|
+
xml: "application/xml",
|
|
22
|
+
zip: "application/zip",
|
|
23
|
+
doc: "application/msword",
|
|
24
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
25
|
+
xls: "application/vnd.ms-excel",
|
|
26
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a string looks like a local file path (not a URL).
|
|
33
|
+
*/
|
|
34
|
+
export function isLocalFilePath(value: string): boolean {
|
|
35
|
+
if (value.startsWith("http://") || value.startsWith("https://") || value.startsWith("data:")) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (value.includes("\n")) return false;
|
|
39
|
+
// Unix absolute path, home dir path, or Windows drive path
|
|
40
|
+
return value.startsWith("/") || value.startsWith("~/") || /^[A-Za-z]:\\/.test(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolvePath(filePath: string): string {
|
|
44
|
+
if (filePath.startsWith("~/")) {
|
|
45
|
+
return resolve(homedir(), filePath.slice(2));
|
|
46
|
+
}
|
|
47
|
+
return resolve(filePath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMimeType(filename: string): string {
|
|
51
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
52
|
+
return EXT_TO_MIME[ext] ?? "application/octet-stream";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Upload a local file to the gateway's temporary storage.
|
|
57
|
+
* Returns the presigned download URL.
|
|
58
|
+
*/
|
|
59
|
+
async function uploadFile(filePath: string): Promise<string> {
|
|
60
|
+
const resolved = resolvePath(filePath);
|
|
61
|
+
const buffer = readFileSync(resolved);
|
|
62
|
+
|
|
63
|
+
if (buffer.length > MAX_UPLOAD_SIZE) {
|
|
64
|
+
throw new Error(`File ${basename(resolved)} exceeds 50MB upload limit (${(buffer.length / (1024 * 1024)).toFixed(1)}MB)`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const filename = basename(resolved);
|
|
68
|
+
const contentType = getMimeType(filename);
|
|
69
|
+
const apiUrl = getApiUrl();
|
|
70
|
+
|
|
71
|
+
const res = await fetch(`${apiUrl}/uploads`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
file: buffer.toString("base64"),
|
|
76
|
+
filename,
|
|
77
|
+
content_type: contentType,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const body = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
83
|
+
throw new Error(`Upload failed: ${body.error ?? res.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await res.json() as { url: string };
|
|
87
|
+
return result.url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Scan input object for local file paths, upload them to temporary storage,
|
|
92
|
+
* and replace with download URLs. Non-file values are left unchanged.
|
|
93
|
+
*/
|
|
94
|
+
export async function uploadLocalFiles(
|
|
95
|
+
input: Record<string, unknown>,
|
|
96
|
+
): Promise<Record<string, unknown>> {
|
|
97
|
+
const result = { ...input };
|
|
98
|
+
|
|
99
|
+
for (const [key, value] of Object.entries(result)) {
|
|
100
|
+
if (typeof value !== "string") continue;
|
|
101
|
+
if (!isLocalFilePath(value)) continue;
|
|
102
|
+
|
|
103
|
+
const resolved = resolvePath(value);
|
|
104
|
+
if (!existsSync(resolved)) continue;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
result[key] = await uploadFile(value);
|
|
108
|
+
} catch {
|
|
109
|
+
// Leave original value if upload fails
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
package/src/core/formatters.ts
CHANGED
|
@@ -152,17 +152,12 @@ export function formatRunResult(result: RunResultLike, opts?: { paymentMethod?:
|
|
|
152
152
|
const latency = result.latency_ms != null ? `${result.latency_ms}ms` : "";
|
|
153
153
|
const method = opts?.paymentMethod ?? "";
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
costStr ? `
|
|
158
|
-
|
|
159
|
-
latency ? `latency: ${latency}` : "",
|
|
160
|
-
].filter(Boolean);
|
|
161
|
-
|
|
162
|
-
lines.push(parts.join(" • "));
|
|
163
|
-
|
|
155
|
+
lines.push(`${status} ${agent}${latency ? ` (${latency})` : ""}`);
|
|
156
|
+
if (costStr) {
|
|
157
|
+
lines.push(`Paid: ${costStr}${method ? ` via ${method}` : ""}`);
|
|
158
|
+
}
|
|
164
159
|
if (result.job_id) {
|
|
165
|
-
lines.push(`
|
|
160
|
+
lines.push(`Job ID: ${result.job_id}`);
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
return lines.join("\n");
|
package/src/core/index.ts
CHANGED
package/src/tools/run.ts
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { apiGet, apiPostWithPayment } from "../core/api-client.js";
|
|
3
|
+
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
4
|
+
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
4
5
|
import { getConfiguredMethods, hasWalletConfigured } from "../core/payments.js";
|
|
6
|
+
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
5
7
|
import { formatRunResult } from "../core/formatters.js";
|
|
6
|
-
import { storeFeedbackToken } from "./_token-cache.js";
|
|
8
|
+
import { storeFeedbackToken, getFeedbackToken } from "./_token-cache.js";
|
|
7
9
|
|
|
8
10
|
function text(t: string) {
|
|
9
11
|
return { content: [{ type: "text" as const, text: t }] };
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
// Pending confirmations: agent_id → { agent, input, method }
|
|
15
|
+
const pendingRuns = new Map<string, {
|
|
16
|
+
agent: { id: string; name: string; price: number };
|
|
17
|
+
input: Record<string, unknown>;
|
|
18
|
+
method?: string;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
12
21
|
export function registerRunTools(server: McpServer): void {
|
|
13
22
|
server.tool(
|
|
14
23
|
"run_agent",
|
|
15
|
-
"Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking.",
|
|
24
|
+
"Run an AI agent from the marketplace. Pays automatically via configured wallet. Returns the agent's output, cost, and job ID for tracking. If spending confirmation is enabled, first call returns a price quote — call again with confirmed: true to execute.",
|
|
16
25
|
{
|
|
17
|
-
agent_id: z.string().describe("Agent ID (UUID or
|
|
26
|
+
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
18
27
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
19
28
|
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
29
|
+
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
20
30
|
},
|
|
21
|
-
async ({ agent_id, input, pay_with }) => {
|
|
31
|
+
async ({ agent_id, input, pay_with, confirmed }) => {
|
|
22
32
|
if (!hasWalletConfigured()) {
|
|
23
33
|
return text(
|
|
24
34
|
"No wallet configured. Set one up first:\n\n" +
|
|
@@ -27,24 +37,45 @@ export function registerRunTools(server: McpServer): void {
|
|
|
27
37
|
);
|
|
28
38
|
}
|
|
29
39
|
|
|
30
|
-
// Resolve
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// Resolve agent and fetch details
|
|
41
|
+
let agent: { id: string; name?: string; pricePer1kTokens?: string; successRate?: number };
|
|
42
|
+
try {
|
|
43
|
+
agent = await apiGet<typeof agent>(`/agents/${agent_id}`);
|
|
44
|
+
} catch {
|
|
45
|
+
return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
|
|
49
|
+
const agentName = agent.name ?? agent_id;
|
|
50
|
+
|
|
51
|
+
// Confirmation step: show price and wait for confirmed: true
|
|
52
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
53
|
+
pendingRuns.set(agent.id, {
|
|
54
|
+
agent: { id: agent.id, name: agentName, price },
|
|
55
|
+
input,
|
|
56
|
+
method: pay_with,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return text([
|
|
60
|
+
`Ready to run ${agentName}`,
|
|
61
|
+
"",
|
|
62
|
+
` Cost: $${price.toFixed(2)}`,
|
|
63
|
+
` Payment: ${pay_with ?? getConfiguredMethods()[0] ?? "auto"}`,
|
|
64
|
+
"",
|
|
65
|
+
"To proceed, call:",
|
|
66
|
+
` run_agent({ agent_id: "${agent.id}", input: <same>, confirmed: true })`,
|
|
67
|
+
"",
|
|
68
|
+
"To cancel, do nothing.",
|
|
69
|
+
].join("\n"));
|
|
40
70
|
}
|
|
41
71
|
|
|
42
72
|
const method = pay_with;
|
|
73
|
+
const processedInput = await uploadLocalFiles(input);
|
|
43
74
|
let result: Record<string, unknown>;
|
|
44
75
|
try {
|
|
45
76
|
result = await apiPostWithPayment<Record<string, unknown>>(
|
|
46
|
-
`/agents/${
|
|
47
|
-
{ input },
|
|
77
|
+
`/agents/${agent.id}/run`,
|
|
78
|
+
{ input: processedInput },
|
|
48
79
|
method,
|
|
49
80
|
);
|
|
50
81
|
} catch (err: unknown) {
|
|
@@ -62,33 +93,68 @@ export function registerRunTools(server: McpServer): void {
|
|
|
62
93
|
}
|
|
63
94
|
return text(`Error: ${msg}`);
|
|
64
95
|
}
|
|
96
|
+
|
|
97
|
+
// Clean up pending confirmation
|
|
98
|
+
pendingRuns.delete(agent.id);
|
|
99
|
+
|
|
65
100
|
const formatted = formatRunResult(result, {
|
|
66
101
|
paymentMethod: method ?? getConfiguredMethods()[0],
|
|
67
102
|
});
|
|
68
103
|
const jobId = (result.job_id as string) ?? "";
|
|
69
|
-
const
|
|
104
|
+
const resultAgentId = (result.agent_id as string) ?? agent.id;
|
|
70
105
|
|
|
71
106
|
if (result.feedback_token) {
|
|
72
|
-
storeFeedbackToken(jobId, result.feedback_token as string,
|
|
107
|
+
storeFeedbackToken(jobId, result.feedback_token as string, resultAgentId);
|
|
73
108
|
}
|
|
74
109
|
|
|
110
|
+
const actualCost = result.cost as number | undefined;
|
|
75
111
|
const status = result.status as string;
|
|
112
|
+
|
|
113
|
+
// Auto-tip if configured and run succeeded
|
|
114
|
+
const defaultTip = getDefaultTipAmount();
|
|
115
|
+
let tipLine = "";
|
|
116
|
+
if (status === "success" && defaultTip > 0 && result.feedback_token) {
|
|
117
|
+
try {
|
|
118
|
+
await apiPost("/tips", {
|
|
119
|
+
job_id: jobId,
|
|
120
|
+
agent_id: resultAgentId,
|
|
121
|
+
amount: defaultTip,
|
|
122
|
+
feedback_token: result.feedback_token,
|
|
123
|
+
});
|
|
124
|
+
tipLine = `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
125
|
+
} catch {
|
|
126
|
+
tipLine = `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
76
130
|
let prompt: string;
|
|
77
131
|
if (status === "success") {
|
|
132
|
+
const costLine = actualCost != null ? `You paid $${actualCost.toFixed(actualCost < 0.01 ? 4 : 2)} for this run.` : "";
|
|
78
133
|
prompt = [
|
|
79
134
|
"",
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
"
|
|
135
|
+
"════════════════════════════════════════",
|
|
136
|
+
costLine,
|
|
137
|
+
tipLine,
|
|
138
|
+
"",
|
|
139
|
+
"Rate this result:",
|
|
140
|
+
` rate_agent({ job_id: "${jobId}", rating: 5, comment: "..." })`,
|
|
141
|
+
...(defaultTip <= 0 ? [
|
|
142
|
+
"",
|
|
143
|
+
"Tip the builder:",
|
|
144
|
+
` tip_agent({ job_id: "${jobId}", agent_id: "${resultAgentId}", amount: 0.25 })`,
|
|
145
|
+
] : []),
|
|
146
|
+
"",
|
|
147
|
+
"Save for later:",
|
|
148
|
+
` favorite_agent({ agent_id: "${resultAgentId}" })`,
|
|
149
|
+
"════════════════════════════════════════",
|
|
85
150
|
].join("\n");
|
|
86
151
|
} else {
|
|
87
152
|
prompt = [
|
|
88
153
|
"",
|
|
89
|
-
"
|
|
90
|
-
"The agent execution failed.
|
|
91
|
-
"
|
|
154
|
+
"════════════════════════════════════════",
|
|
155
|
+
"The agent execution failed.",
|
|
156
|
+
"A refund has been initiated automatically.",
|
|
157
|
+
"════════════════════════════════════════",
|
|
92
158
|
].join("\n");
|
|
93
159
|
}
|
|
94
160
|
return text(formatted + prompt);
|
package/src/tools/solve.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
getConfiguredMethods,
|
|
7
7
|
getAcceptedPaymentMethods,
|
|
8
8
|
} from "../core/payments.js";
|
|
9
|
+
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
9
10
|
import { agentList, formatRunResult } from "../core/formatters.js";
|
|
11
|
+
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
10
12
|
import type { AgentRecord } from "../core/types.js";
|
|
11
13
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
12
14
|
|
|
@@ -14,21 +16,49 @@ function text(t: string) {
|
|
|
14
16
|
return { content: [{ type: "text" as const, text: t }] };
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
function
|
|
19
|
+
function feedbackPrompt(jobId: string, agentId: string, agentName: string, cost?: number, tipSent?: string): string {
|
|
20
|
+
const defaultTip = getDefaultTipAmount();
|
|
21
|
+
const costLine = cost != null ? `You paid $${cost.toFixed(cost < 0.01 ? 4 : 2)} for this run.` : "";
|
|
18
22
|
return [
|
|
19
23
|
"",
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
24
|
+
"════════════════════════════════════════",
|
|
25
|
+
costLine,
|
|
26
|
+
tipSent ?? "",
|
|
27
|
+
"",
|
|
28
|
+
"Rate this result:",
|
|
29
|
+
` rate_agent({ job_id: "${jobId}", rating: 5, comment: "..." })`,
|
|
30
|
+
...(defaultTip <= 0 ? [
|
|
31
|
+
"",
|
|
32
|
+
"Tip the builder:",
|
|
33
|
+
` tip_agent({ job_id: "${jobId}", agent_id: "${agentId}", amount: 0.25 })`,
|
|
34
|
+
] : []),
|
|
35
|
+
"",
|
|
36
|
+
"Save for later:",
|
|
37
|
+
` favorite_agent({ agent_id: "${agentId}" })`,
|
|
38
|
+
"════════════════════════════════════════",
|
|
25
39
|
].join("\n");
|
|
26
40
|
}
|
|
27
41
|
|
|
42
|
+
async function autoTip(jobId: string, agentId: string, agentName: string, feedbackToken: string): Promise<string> {
|
|
43
|
+
const defaultTip = getDefaultTipAmount();
|
|
44
|
+
if (defaultTip <= 0) return "";
|
|
45
|
+
try {
|
|
46
|
+
await apiPost("/tips", {
|
|
47
|
+
job_id: jobId,
|
|
48
|
+
agent_id: agentId,
|
|
49
|
+
amount: defaultTip,
|
|
50
|
+
feedback_token: feedbackToken,
|
|
51
|
+
});
|
|
52
|
+
return `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
53
|
+
} catch {
|
|
54
|
+
return `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
export function registerSolveTools(server: McpServer): void {
|
|
29
59
|
server.tool(
|
|
30
60
|
"solve",
|
|
31
|
-
"Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace
|
|
61
|
+
"Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace. If spending confirmation is enabled, returns a price quote first — call again with confirmed: true to execute.",
|
|
32
62
|
{
|
|
33
63
|
intent: z
|
|
34
64
|
.string()
|
|
@@ -51,8 +81,12 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
51
81
|
.describe(
|
|
52
82
|
"Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted.",
|
|
53
83
|
),
|
|
84
|
+
confirmed: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
54
88
|
},
|
|
55
|
-
async ({ intent, input, budget, pay_with }) => {
|
|
89
|
+
async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
56
90
|
if (!hasWalletConfigured()) {
|
|
57
91
|
return text(
|
|
58
92
|
"No wallet configured. Set one up first:\n\n" +
|
|
@@ -65,19 +99,23 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
65
99
|
|
|
66
100
|
// Path 1: If authenticated, use the platform /solve route
|
|
67
101
|
try {
|
|
102
|
+
const processedInput = await uploadLocalFiles(input);
|
|
68
103
|
const result = await apiPost<Record<string, unknown>>("/solve", {
|
|
69
104
|
intent,
|
|
70
|
-
input,
|
|
105
|
+
input: processedInput,
|
|
71
106
|
budget,
|
|
72
107
|
});
|
|
73
108
|
const jobId = (result as Record<string, unknown>).job_id as string ?? "";
|
|
74
109
|
const agentId = (result as Record<string, unknown>).agent_id as string ?? "";
|
|
110
|
+
const agentName = (result as Record<string, unknown>).agent_name as string ?? "";
|
|
75
111
|
|
|
76
112
|
if (result.feedback_token) {
|
|
77
113
|
storeFeedbackToken(jobId, result.feedback_token as string, agentId);
|
|
78
114
|
}
|
|
79
115
|
|
|
80
|
-
|
|
116
|
+
const cost = (result as Record<string, unknown>).cost as number | undefined;
|
|
117
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token as string) : "";
|
|
118
|
+
return text(formatRunResult(result) + feedbackPrompt(jobId, agentId, agentName, cost, tipMsg));
|
|
81
119
|
} catch (err: unknown) {
|
|
82
120
|
const isAuthError =
|
|
83
121
|
err instanceof Error &&
|
|
@@ -111,18 +149,34 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
111
149
|
});
|
|
112
150
|
const selected = affordable[0] ?? agents[0];
|
|
113
151
|
|
|
114
|
-
// Estimate cost for the selected agent
|
|
115
152
|
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
116
153
|
const estimatedCost =
|
|
117
154
|
selected.pricingModel === "fixed"
|
|
118
155
|
? selectedPrice
|
|
119
156
|
: (inputTokens / 1000) * selectedPrice;
|
|
120
157
|
|
|
158
|
+
// Confirmation step: show discovery + price, wait for confirmed: true
|
|
159
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
160
|
+
return text([
|
|
161
|
+
discovery,
|
|
162
|
+
"",
|
|
163
|
+
`Best match: ${selected.name}`,
|
|
164
|
+
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
165
|
+
`Payment: ${method}`,
|
|
166
|
+
"",
|
|
167
|
+
"To proceed, call:",
|
|
168
|
+
` solve({ intent: "${intent}", input: <same>, budget: ${budget}, confirmed: true })`,
|
|
169
|
+
"",
|
|
170
|
+
"To cancel, do nothing.",
|
|
171
|
+
].join("\n"));
|
|
172
|
+
}
|
|
173
|
+
|
|
121
174
|
let result: Record<string, unknown>;
|
|
175
|
+
const processedInput2 = await uploadLocalFiles(input);
|
|
122
176
|
try {
|
|
123
177
|
result = await apiPostWithPayment<Record<string, unknown>>(
|
|
124
178
|
`/agents/${selected.id}/run`,
|
|
125
|
-
{ input },
|
|
179
|
+
{ input: processedInput2 },
|
|
126
180
|
method,
|
|
127
181
|
);
|
|
128
182
|
} catch (err: unknown) {
|
|
@@ -137,7 +191,6 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
137
191
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
138
192
|
}
|
|
139
193
|
|
|
140
|
-
// Cache feedback token if present
|
|
141
194
|
const jobId = (result as Record<string, unknown>).job_id as string ?? "";
|
|
142
195
|
const agentId2 = (result as Record<string, unknown>).agent_id as string ?? selected.id;
|
|
143
196
|
|
|
@@ -145,7 +198,9 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
145
198
|
storeFeedbackToken(jobId, result.feedback_token as string, agentId2);
|
|
146
199
|
}
|
|
147
200
|
|
|
148
|
-
|
|
201
|
+
const actualCost = (result as Record<string, unknown>).cost as number | undefined;
|
|
202
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId2, selected.name ?? "", result.feedback_token as string) : "";
|
|
203
|
+
|
|
149
204
|
const output = [
|
|
150
205
|
discovery,
|
|
151
206
|
"",
|
|
@@ -153,7 +208,7 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
153
208
|
`Estimated cost: $${estimatedCost.toFixed(4)}`,
|
|
154
209
|
"",
|
|
155
210
|
formatRunResult(result, { paymentMethod: method }),
|
|
156
|
-
|
|
211
|
+
feedbackPrompt(jobId, agentId2, selected.name ?? "", actualCost, tipMsg),
|
|
157
212
|
].join("\n");
|
|
158
213
|
|
|
159
214
|
return text(output);
|