@agentwonderland/mcp 0.1.4 → 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/formatters.js +5 -8
- package/dist/tools/run.js +79 -26
- package/dist/tools/solve.js +64 -13
- package/package.json +1 -1
- package/src/core/config.ts +20 -1
- package/src/core/formatters.ts +5 -10
- package/src/tools/run.ts +90 -26
- package/src/tools/solve.ts +65 -13
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.
|
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/tools/run.js
CHANGED
|
@@ -1,40 +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
3
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
4
4
|
import { getConfiguredMethods, hasWalletConfigured } from "../core/payments.js";
|
|
5
|
+
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
5
6
|
import { formatRunResult } from "../core/formatters.js";
|
|
6
7
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
7
8
|
function text(t) {
|
|
8
9
|
return { content: [{ type: "text", text: t }] };
|
|
9
10
|
}
|
|
11
|
+
// Pending confirmations: agent_id → { agent, input, method }
|
|
12
|
+
const pendingRuns = new Map();
|
|
10
13
|
export function registerRunTools(server) {
|
|
11
|
-
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.", {
|
|
12
|
-
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)"),
|
|
13
16
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
14
17
|
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
15
|
-
|
|
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 }) => {
|
|
16
20
|
if (!hasWalletConfigured()) {
|
|
17
21
|
return text("No wallet configured. Set one up first:\n\n" +
|
|
18
22
|
' wallet_setup({ action: "create", name: "my-wallet" })\n\n' +
|
|
19
23
|
"Then fund it with USDC on Tempo and try again.");
|
|
20
24
|
}
|
|
21
|
-
// Resolve
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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"));
|
|
32
53
|
}
|
|
33
54
|
const method = pay_with;
|
|
34
55
|
const processedInput = await uploadLocalFiles(input);
|
|
35
56
|
let result;
|
|
36
57
|
try {
|
|
37
|
-
result = await apiPostWithPayment(`/agents/${
|
|
58
|
+
result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
|
|
38
59
|
}
|
|
39
60
|
catch (err) {
|
|
40
61
|
const apiErr = err;
|
|
@@ -49,32 +70,64 @@ export function registerRunTools(server) {
|
|
|
49
70
|
}
|
|
50
71
|
return text(`Error: ${msg}`);
|
|
51
72
|
}
|
|
73
|
+
// Clean up pending confirmation
|
|
74
|
+
pendingRuns.delete(agent.id);
|
|
52
75
|
const formatted = formatRunResult(result, {
|
|
53
76
|
paymentMethod: method ?? getConfiguredMethods()[0],
|
|
54
77
|
});
|
|
55
78
|
const jobId = result.job_id ?? "";
|
|
56
|
-
const
|
|
79
|
+
const resultAgentId = result.agent_id ?? agent.id;
|
|
57
80
|
if (result.feedback_token) {
|
|
58
|
-
storeFeedbackToken(jobId, result.feedback_token,
|
|
81
|
+
storeFeedbackToken(jobId, result.feedback_token, resultAgentId);
|
|
59
82
|
}
|
|
83
|
+
const actualCost = result.cost;
|
|
60
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
|
+
}
|
|
61
102
|
let prompt;
|
|
62
103
|
if (status === "success") {
|
|
104
|
+
const costLine = actualCost != null ? `You paid $${actualCost.toFixed(actualCost < 0.01 ? 4 : 2)} for this run.` : "";
|
|
63
105
|
prompt = [
|
|
64
106
|
"",
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
"
|
|
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
|
+
"════════════════════════════════════════",
|
|
70
122
|
].join("\n");
|
|
71
123
|
}
|
|
72
124
|
else {
|
|
73
125
|
prompt = [
|
|
74
126
|
"",
|
|
75
|
-
"
|
|
76
|
-
"The agent execution failed.
|
|
77
|
-
"
|
|
127
|
+
"════════════════════════════════════════",
|
|
128
|
+
"The agent execution failed.",
|
|
129
|
+
"A refund has been initiated automatically.",
|
|
130
|
+
"════════════════════════════════════════",
|
|
78
131
|
].join("\n");
|
|
79
132
|
}
|
|
80
133
|
return text(formatted + prompt);
|
package/dist/tools/solve.js
CHANGED
|
@@ -1,24 +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";
|
|
5
6
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
6
7
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
7
8
|
function text(t) {
|
|
8
9
|
return { content: [{ type: "text", text: t }] };
|
|
9
10
|
}
|
|
10
|
-
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.` : "";
|
|
11
14
|
return [
|
|
12
15
|
"",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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
|
+
"════════════════════════════════════════",
|
|
18
31
|
].join("\n");
|
|
19
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
|
+
}
|
|
20
50
|
export function registerSolveTools(server) {
|
|
21
|
-
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.", {
|
|
22
52
|
intent: z
|
|
23
53
|
.string()
|
|
24
54
|
.describe("What you want to accomplish (natural language)"),
|
|
@@ -38,7 +68,11 @@ export function registerSolveTools(server) {
|
|
|
38
68
|
.string()
|
|
39
69
|
.optional()
|
|
40
70
|
.describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
41
|
-
|
|
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 }) => {
|
|
42
76
|
if (!hasWalletConfigured()) {
|
|
43
77
|
return text("No wallet configured. Set one up first:\n\n" +
|
|
44
78
|
' wallet_setup({ action: "create", name: "my-wallet" })\n\n' +
|
|
@@ -55,10 +89,13 @@ export function registerSolveTools(server) {
|
|
|
55
89
|
});
|
|
56
90
|
const jobId = result.job_id ?? "";
|
|
57
91
|
const agentId = result.agent_id ?? "";
|
|
92
|
+
const agentName = result.agent_name ?? "";
|
|
58
93
|
if (result.feedback_token) {
|
|
59
94
|
storeFeedbackToken(jobId, result.feedback_token, agentId);
|
|
60
95
|
}
|
|
61
|
-
|
|
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));
|
|
62
99
|
}
|
|
63
100
|
catch (err) {
|
|
64
101
|
const isAuthError = err instanceof Error &&
|
|
@@ -87,11 +124,25 @@ export function registerSolveTools(server) {
|
|
|
87
124
|
return cost <= budget;
|
|
88
125
|
});
|
|
89
126
|
const selected = affordable[0] ?? agents[0];
|
|
90
|
-
// Estimate cost for the selected agent
|
|
91
127
|
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
92
128
|
const estimatedCost = selected.pricingModel === "fixed"
|
|
93
129
|
? selectedPrice
|
|
94
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
|
+
}
|
|
95
146
|
let result;
|
|
96
147
|
const processedInput2 = await uploadLocalFiles(input);
|
|
97
148
|
try {
|
|
@@ -106,13 +157,13 @@ export function registerSolveTools(server) {
|
|
|
106
157
|
}
|
|
107
158
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
108
159
|
}
|
|
109
|
-
// Cache feedback token if present
|
|
110
160
|
const jobId = result.job_id ?? "";
|
|
111
161
|
const agentId2 = result.agent_id ?? selected.id;
|
|
112
162
|
if (result.feedback_token) {
|
|
113
163
|
storeFeedbackToken(jobId, result.feedback_token, agentId2);
|
|
114
164
|
}
|
|
115
|
-
|
|
165
|
+
const actualCost = result.cost;
|
|
166
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId2, selected.name ?? "", result.feedback_token) : "";
|
|
116
167
|
const output = [
|
|
117
168
|
discovery,
|
|
118
169
|
"",
|
|
@@ -120,7 +171,7 @@ export function registerSolveTools(server) {
|
|
|
120
171
|
`Estimated cost: $${estimatedCost.toFixed(4)}`,
|
|
121
172
|
"",
|
|
122
173
|
formatRunResult(result, { paymentMethod: method }),
|
|
123
|
-
|
|
174
|
+
feedbackPrompt(jobId, agentId2, selected.name ?? "", actualCost, tipMsg),
|
|
124
175
|
].join("\n");
|
|
125
176
|
return text(output);
|
|
126
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
|
/**
|
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/tools/run.ts
CHANGED
|
@@ -1,25 +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
4
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
5
5
|
import { getConfiguredMethods, hasWalletConfigured } from "../core/payments.js";
|
|
6
|
+
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
6
7
|
import { formatRunResult } from "../core/formatters.js";
|
|
7
|
-
import { storeFeedbackToken } from "./_token-cache.js";
|
|
8
|
+
import { storeFeedbackToken, getFeedbackToken } from "./_token-cache.js";
|
|
8
9
|
|
|
9
10
|
function text(t: string) {
|
|
10
11
|
return { content: [{ type: "text" as const, text: t }] };
|
|
11
12
|
}
|
|
12
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
|
+
|
|
13
21
|
export function registerRunTools(server: McpServer): void {
|
|
14
22
|
server.tool(
|
|
15
23
|
"run_agent",
|
|
16
|
-
"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.",
|
|
17
25
|
{
|
|
18
|
-
agent_id: z.string().describe("Agent ID (UUID or
|
|
26
|
+
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
19
27
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
20
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."),
|
|
21
30
|
},
|
|
22
|
-
async ({ agent_id, input, pay_with }) => {
|
|
31
|
+
async ({ agent_id, input, pay_with, confirmed }) => {
|
|
23
32
|
if (!hasWalletConfigured()) {
|
|
24
33
|
return text(
|
|
25
34
|
"No wallet configured. Set one up first:\n\n" +
|
|
@@ -28,16 +37,36 @@ export function registerRunTools(server: McpServer): void {
|
|
|
28
37
|
);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
// Resolve
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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"));
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
const method = pay_with;
|
|
@@ -45,7 +74,7 @@ export function registerRunTools(server: McpServer): void {
|
|
|
45
74
|
let result: Record<string, unknown>;
|
|
46
75
|
try {
|
|
47
76
|
result = await apiPostWithPayment<Record<string, unknown>>(
|
|
48
|
-
`/agents/${
|
|
77
|
+
`/agents/${agent.id}/run`,
|
|
49
78
|
{ input: processedInput },
|
|
50
79
|
method,
|
|
51
80
|
);
|
|
@@ -64,33 +93,68 @@ export function registerRunTools(server: McpServer): void {
|
|
|
64
93
|
}
|
|
65
94
|
return text(`Error: ${msg}`);
|
|
66
95
|
}
|
|
96
|
+
|
|
97
|
+
// Clean up pending confirmation
|
|
98
|
+
pendingRuns.delete(agent.id);
|
|
99
|
+
|
|
67
100
|
const formatted = formatRunResult(result, {
|
|
68
101
|
paymentMethod: method ?? getConfiguredMethods()[0],
|
|
69
102
|
});
|
|
70
103
|
const jobId = (result.job_id as string) ?? "";
|
|
71
|
-
const
|
|
104
|
+
const resultAgentId = (result.agent_id as string) ?? agent.id;
|
|
72
105
|
|
|
73
106
|
if (result.feedback_token) {
|
|
74
|
-
storeFeedbackToken(jobId, result.feedback_token as string,
|
|
107
|
+
storeFeedbackToken(jobId, result.feedback_token as string, resultAgentId);
|
|
75
108
|
}
|
|
76
109
|
|
|
110
|
+
const actualCost = result.cost as number | undefined;
|
|
77
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
|
+
|
|
78
130
|
let prompt: string;
|
|
79
131
|
if (status === "success") {
|
|
132
|
+
const costLine = actualCost != null ? `You paid $${actualCost.toFixed(actualCost < 0.01 ? 4 : 2)} for this run.` : "";
|
|
80
133
|
prompt = [
|
|
81
134
|
"",
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
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
|
+
"════════════════════════════════════════",
|
|
87
150
|
].join("\n");
|
|
88
151
|
} else {
|
|
89
152
|
prompt = [
|
|
90
153
|
"",
|
|
91
|
-
"
|
|
92
|
-
"The agent execution failed.
|
|
93
|
-
"
|
|
154
|
+
"════════════════════════════════════════",
|
|
155
|
+
"The agent execution failed.",
|
|
156
|
+
"A refund has been initiated automatically.",
|
|
157
|
+
"════════════════════════════════════════",
|
|
94
158
|
].join("\n");
|
|
95
159
|
}
|
|
96
160
|
return text(formatted + prompt);
|
package/src/tools/solve.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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";
|
|
10
11
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
11
12
|
import type { AgentRecord } from "../core/types.js";
|
|
@@ -15,21 +16,49 @@ function text(t: string) {
|
|
|
15
16
|
return { content: [{ type: "text" as const, text: t }] };
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
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.` : "";
|
|
19
22
|
return [
|
|
20
23
|
"",
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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
|
+
"════════════════════════════════════════",
|
|
26
39
|
].join("\n");
|
|
27
40
|
}
|
|
28
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
|
+
|
|
29
58
|
export function registerSolveTools(server: McpServer): void {
|
|
30
59
|
server.tool(
|
|
31
60
|
"solve",
|
|
32
|
-
"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.",
|
|
33
62
|
{
|
|
34
63
|
intent: z
|
|
35
64
|
.string()
|
|
@@ -52,8 +81,12 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
52
81
|
.describe(
|
|
53
82
|
"Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted.",
|
|
54
83
|
),
|
|
84
|
+
confirmed: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
55
88
|
},
|
|
56
|
-
async ({ intent, input, budget, pay_with }) => {
|
|
89
|
+
async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
57
90
|
if (!hasWalletConfigured()) {
|
|
58
91
|
return text(
|
|
59
92
|
"No wallet configured. Set one up first:\n\n" +
|
|
@@ -74,12 +107,15 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
74
107
|
});
|
|
75
108
|
const jobId = (result as Record<string, unknown>).job_id as string ?? "";
|
|
76
109
|
const agentId = (result as Record<string, unknown>).agent_id as string ?? "";
|
|
110
|
+
const agentName = (result as Record<string, unknown>).agent_name as string ?? "";
|
|
77
111
|
|
|
78
112
|
if (result.feedback_token) {
|
|
79
113
|
storeFeedbackToken(jobId, result.feedback_token as string, agentId);
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
|
|
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));
|
|
83
119
|
} catch (err: unknown) {
|
|
84
120
|
const isAuthError =
|
|
85
121
|
err instanceof Error &&
|
|
@@ -113,13 +149,28 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
113
149
|
});
|
|
114
150
|
const selected = affordable[0] ?? agents[0];
|
|
115
151
|
|
|
116
|
-
// Estimate cost for the selected agent
|
|
117
152
|
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
118
153
|
const estimatedCost =
|
|
119
154
|
selected.pricingModel === "fixed"
|
|
120
155
|
? selectedPrice
|
|
121
156
|
: (inputTokens / 1000) * selectedPrice;
|
|
122
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
|
+
|
|
123
174
|
let result: Record<string, unknown>;
|
|
124
175
|
const processedInput2 = await uploadLocalFiles(input);
|
|
125
176
|
try {
|
|
@@ -140,7 +191,6 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
140
191
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
141
192
|
}
|
|
142
193
|
|
|
143
|
-
// Cache feedback token if present
|
|
144
194
|
const jobId = (result as Record<string, unknown>).job_id as string ?? "";
|
|
145
195
|
const agentId2 = (result as Record<string, unknown>).agent_id as string ?? selected.id;
|
|
146
196
|
|
|
@@ -148,7 +198,9 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
148
198
|
storeFeedbackToken(jobId, result.feedback_token as string, agentId2);
|
|
149
199
|
}
|
|
150
200
|
|
|
151
|
-
|
|
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
|
+
|
|
152
204
|
const output = [
|
|
153
205
|
discovery,
|
|
154
206
|
"",
|
|
@@ -156,7 +208,7 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
156
208
|
`Estimated cost: $${estimatedCost.toFixed(4)}`,
|
|
157
209
|
"",
|
|
158
210
|
formatRunResult(result, { paymentMethod: method }),
|
|
159
|
-
|
|
211
|
+
feedbackPrompt(jobId, agentId2, selected.name ?? "", actualCost, tipMsg),
|
|
160
212
|
].join("\n");
|
|
161
213
|
|
|
162
214
|
return text(output);
|