@agentwonderland/mcp 0.1.4 → 0.1.6
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 -36
- package/dist/tools/solve.js +60 -14
- package/package.json +1 -1
- package/src/core/config.ts +20 -1
- package/src/core/formatters.ts +5 -10
- package/src/tools/run.ts +98 -36
- package/src/tools/solve.ts +62 -14
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,64 @@
|
|
|
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
|
+
function multiText(...blocks) {
|
|
12
|
+
return { content: blocks.map((t) => ({ type: "text", text: t })) };
|
|
13
|
+
}
|
|
14
|
+
// Pending confirmations: agent_id → { agent, input, method }
|
|
15
|
+
const pendingRuns = new Map();
|
|
10
16
|
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
|
|
17
|
+
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.", {
|
|
18
|
+
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
13
19
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
14
20
|
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
15
|
-
|
|
21
|
+
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
22
|
+
}, async ({ agent_id, input, pay_with, confirmed }) => {
|
|
16
23
|
if (!hasWalletConfigured()) {
|
|
17
24
|
return text("No wallet configured. Set one up first:\n\n" +
|
|
18
25
|
' wallet_setup({ action: "create", name: "my-wallet" })\n\n' +
|
|
19
26
|
"Then fund it with USDC on Tempo and try again.");
|
|
20
27
|
}
|
|
21
|
-
// Resolve
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// Resolve agent and fetch details
|
|
29
|
+
let agent;
|
|
30
|
+
try {
|
|
31
|
+
agent = await apiGet(`/agents/${agent_id}`);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
|
|
35
|
+
}
|
|
36
|
+
const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
|
|
37
|
+
const agentName = agent.name ?? agent_id;
|
|
38
|
+
// Confirmation step: show price and wait for confirmed: true
|
|
39
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
40
|
+
pendingRuns.set(agent.id, {
|
|
41
|
+
agent: { id: agent.id, name: agentName, price },
|
|
42
|
+
input,
|
|
43
|
+
method: pay_with,
|
|
44
|
+
});
|
|
45
|
+
return text([
|
|
46
|
+
`Ready to run ${agentName}`,
|
|
47
|
+
"",
|
|
48
|
+
` Cost: $${price.toFixed(2)}`,
|
|
49
|
+
` Payment: ${pay_with ?? getConfiguredMethods()[0] ?? "auto"}`,
|
|
50
|
+
"",
|
|
51
|
+
"To proceed, call:",
|
|
52
|
+
` run_agent({ agent_id: "${agent.id}", input: <same>, confirmed: true })`,
|
|
53
|
+
"",
|
|
54
|
+
"To cancel, do nothing.",
|
|
55
|
+
].join("\n"));
|
|
32
56
|
}
|
|
33
57
|
const method = pay_with;
|
|
34
58
|
const processedInput = await uploadLocalFiles(input);
|
|
35
59
|
let result;
|
|
36
60
|
try {
|
|
37
|
-
result = await apiPostWithPayment(`/agents/${
|
|
61
|
+
result = await apiPostWithPayment(`/agents/${agent.id}/run`, { input: processedInput }, method);
|
|
38
62
|
}
|
|
39
63
|
catch (err) {
|
|
40
64
|
const apiErr = err;
|
|
@@ -49,34 +73,53 @@ export function registerRunTools(server) {
|
|
|
49
73
|
}
|
|
50
74
|
return text(`Error: ${msg}`);
|
|
51
75
|
}
|
|
76
|
+
// Clean up pending confirmation
|
|
77
|
+
pendingRuns.delete(agent.id);
|
|
52
78
|
const formatted = formatRunResult(result, {
|
|
53
79
|
paymentMethod: method ?? getConfiguredMethods()[0],
|
|
54
80
|
});
|
|
55
81
|
const jobId = result.job_id ?? "";
|
|
56
|
-
const
|
|
82
|
+
const resultAgentId = result.agent_id ?? agent.id;
|
|
57
83
|
if (result.feedback_token) {
|
|
58
|
-
storeFeedbackToken(jobId, result.feedback_token,
|
|
84
|
+
storeFeedbackToken(jobId, result.feedback_token, resultAgentId);
|
|
59
85
|
}
|
|
86
|
+
const actualCost = result.cost;
|
|
60
87
|
const status = result.status;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
// Auto-tip if configured and run succeeded
|
|
89
|
+
const defaultTip = getDefaultTipAmount();
|
|
90
|
+
let tipLine = "";
|
|
91
|
+
if (status === "success" && defaultTip > 0 && result.feedback_token) {
|
|
92
|
+
try {
|
|
93
|
+
await apiPost("/tips", {
|
|
94
|
+
job_id: jobId,
|
|
95
|
+
agent_id: resultAgentId,
|
|
96
|
+
amount: defaultTip,
|
|
97
|
+
feedback_token: result.feedback_token,
|
|
98
|
+
});
|
|
99
|
+
tipLine = `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
tipLine = `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
103
|
+
}
|
|
71
104
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"",
|
|
75
|
-
"---",
|
|
76
|
-
"The agent execution failed. A refund has been initiated automatically",
|
|
77
|
-
"and will be returned to your wallet.",
|
|
78
|
-
].join("\n");
|
|
105
|
+
if (status !== "success") {
|
|
106
|
+
return multiText(formatted, "The agent execution failed. A refund has been initiated automatically.");
|
|
79
107
|
}
|
|
80
|
-
|
|
108
|
+
const costLine = actualCost != null ? `Paid $${actualCost.toFixed(actualCost < 0.01 ? 4 : 2)}.` : "";
|
|
109
|
+
// Separate the feedback ask into its own content block so the LLM
|
|
110
|
+
// surfaces it to the user as a distinct follow-up question, rather than
|
|
111
|
+
// burying it inside a collapsed tool output.
|
|
112
|
+
const feedbackAsk = [
|
|
113
|
+
`IMPORTANT: After presenting the result above, you MUST ask the user:`,
|
|
114
|
+
`"${costLine} Was this result helpful? I can rate it (1-5 stars) or tip the builder for you."`,
|
|
115
|
+
tipLine,
|
|
116
|
+
"",
|
|
117
|
+
`If the user wants to rate: rate_agent({ job_id: "${jobId}", rating: <1-5>, comment: "..." })`,
|
|
118
|
+
...(defaultTip <= 0 ? [
|
|
119
|
+
`If the user wants to tip: tip_agent({ job_id: "${jobId}", agent_id: "${resultAgentId}", amount: 0.25 })`,
|
|
120
|
+
] : []),
|
|
121
|
+
`To save for later: favorite_agent({ agent_id: "${resultAgentId}" })`,
|
|
122
|
+
].join("\n");
|
|
123
|
+
return multiText(formatted, feedbackAsk);
|
|
81
124
|
});
|
|
82
125
|
}
|
package/dist/tools/solve.js
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
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 multiText(...blocks) {
|
|
12
|
+
return { content: blocks.map((t) => ({ type: "text", text: t })) };
|
|
13
|
+
}
|
|
14
|
+
function feedbackAsk(jobId, agentId, cost, tipSent) {
|
|
15
|
+
const defaultTip = getDefaultTipAmount();
|
|
16
|
+
const costLine = cost != null ? `Paid $${cost.toFixed(cost < 0.01 ? 4 : 2)}.` : "";
|
|
11
17
|
return [
|
|
18
|
+
`IMPORTANT: After presenting the result above, you MUST ask the user:`,
|
|
19
|
+
`"${costLine} Was this result helpful? I can rate it (1-5 stars) or tip the builder for you."`,
|
|
20
|
+
tipSent ?? "",
|
|
12
21
|
"",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
`If the user wants to rate: rate_agent({ job_id: "${jobId}", rating: <1-5>, comment: "..." })`,
|
|
23
|
+
...(defaultTip <= 0 ? [
|
|
24
|
+
`If the user wants to tip: tip_agent({ job_id: "${jobId}", agent_id: "${agentId}", amount: 0.25 })`,
|
|
25
|
+
] : []),
|
|
26
|
+
`To save for later: favorite_agent({ agent_id: "${agentId}" })`,
|
|
18
27
|
].join("\n");
|
|
19
28
|
}
|
|
29
|
+
async function autoTip(jobId, agentId, agentName, feedbackToken) {
|
|
30
|
+
const defaultTip = getDefaultTipAmount();
|
|
31
|
+
if (defaultTip <= 0)
|
|
32
|
+
return "";
|
|
33
|
+
try {
|
|
34
|
+
await apiPost("/tips", {
|
|
35
|
+
job_id: jobId,
|
|
36
|
+
agent_id: agentId,
|
|
37
|
+
amount: defaultTip,
|
|
38
|
+
feedback_token: feedbackToken,
|
|
39
|
+
});
|
|
40
|
+
return `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
20
46
|
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
|
|
47
|
+
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
48
|
intent: z
|
|
23
49
|
.string()
|
|
24
50
|
.describe("What you want to accomplish (natural language)"),
|
|
@@ -38,7 +64,11 @@ export function registerSolveTools(server) {
|
|
|
38
64
|
.string()
|
|
39
65
|
.optional()
|
|
40
66
|
.describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
41
|
-
|
|
67
|
+
confirmed: z
|
|
68
|
+
.boolean()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
71
|
+
}, async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
42
72
|
if (!hasWalletConfigured()) {
|
|
43
73
|
return text("No wallet configured. Set one up first:\n\n" +
|
|
44
74
|
' wallet_setup({ action: "create", name: "my-wallet" })\n\n' +
|
|
@@ -55,10 +85,13 @@ export function registerSolveTools(server) {
|
|
|
55
85
|
});
|
|
56
86
|
const jobId = result.job_id ?? "";
|
|
57
87
|
const agentId = result.agent_id ?? "";
|
|
88
|
+
const agentName = result.agent_name ?? "";
|
|
58
89
|
if (result.feedback_token) {
|
|
59
90
|
storeFeedbackToken(jobId, result.feedback_token, agentId);
|
|
60
91
|
}
|
|
61
|
-
|
|
92
|
+
const cost = result.cost;
|
|
93
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token) : "";
|
|
94
|
+
return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, cost, tipMsg));
|
|
62
95
|
}
|
|
63
96
|
catch (err) {
|
|
64
97
|
const isAuthError = err instanceof Error &&
|
|
@@ -87,11 +120,25 @@ export function registerSolveTools(server) {
|
|
|
87
120
|
return cost <= budget;
|
|
88
121
|
});
|
|
89
122
|
const selected = affordable[0] ?? agents[0];
|
|
90
|
-
// Estimate cost for the selected agent
|
|
91
123
|
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
92
124
|
const estimatedCost = selected.pricingModel === "fixed"
|
|
93
125
|
? selectedPrice
|
|
94
126
|
: (inputTokens / 1000) * selectedPrice;
|
|
127
|
+
// Confirmation step: show discovery + price, wait for confirmed: true
|
|
128
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
129
|
+
return text([
|
|
130
|
+
discovery,
|
|
131
|
+
"",
|
|
132
|
+
`Best match: ${selected.name}`,
|
|
133
|
+
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
134
|
+
`Payment: ${method}`,
|
|
135
|
+
"",
|
|
136
|
+
"To proceed, call:",
|
|
137
|
+
` solve({ intent: "${intent}", input: <same>, budget: ${budget}, confirmed: true })`,
|
|
138
|
+
"",
|
|
139
|
+
"To cancel, do nothing.",
|
|
140
|
+
].join("\n"));
|
|
141
|
+
}
|
|
95
142
|
let result;
|
|
96
143
|
const processedInput2 = await uploadLocalFiles(input);
|
|
97
144
|
try {
|
|
@@ -106,13 +153,13 @@ export function registerSolveTools(server) {
|
|
|
106
153
|
}
|
|
107
154
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
108
155
|
}
|
|
109
|
-
// Cache feedback token if present
|
|
110
156
|
const jobId = result.job_id ?? "";
|
|
111
157
|
const agentId2 = result.agent_id ?? selected.id;
|
|
112
158
|
if (result.feedback_token) {
|
|
113
159
|
storeFeedbackToken(jobId, result.feedback_token, agentId2);
|
|
114
160
|
}
|
|
115
|
-
|
|
161
|
+
const actualCost = result.cost;
|
|
162
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId2, selected.name ?? "", result.feedback_token) : "";
|
|
116
163
|
const output = [
|
|
117
164
|
discovery,
|
|
118
165
|
"",
|
|
@@ -120,8 +167,7 @@ export function registerSolveTools(server) {
|
|
|
120
167
|
`Estimated cost: $${estimatedCost.toFixed(4)}`,
|
|
121
168
|
"",
|
|
122
169
|
formatRunResult(result, { paymentMethod: method }),
|
|
123
|
-
ratingPrompt(jobId),
|
|
124
170
|
].join("\n");
|
|
125
|
-
return
|
|
171
|
+
return multiText(output, feedbackAsk(jobId, agentId2, actualCost, tipMsg));
|
|
126
172
|
});
|
|
127
173
|
}
|
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,38 @@
|
|
|
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
|
+
function multiText(...blocks: string[]) {
|
|
15
|
+
return { content: blocks.map((t) => ({ type: "text" as const, text: t })) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Pending confirmations: agent_id → { agent, input, method }
|
|
19
|
+
const pendingRuns = new Map<string, {
|
|
20
|
+
agent: { id: string; name: string; price: number };
|
|
21
|
+
input: Record<string, unknown>;
|
|
22
|
+
method?: string;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
13
25
|
export function registerRunTools(server: McpServer): void {
|
|
14
26
|
server.tool(
|
|
15
27
|
"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.",
|
|
28
|
+
"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
29
|
{
|
|
18
|
-
agent_id: z.string().describe("Agent ID (UUID or
|
|
30
|
+
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
19
31
|
input: z.record(z.unknown()).describe("Input payload for the agent"),
|
|
20
32
|
pay_with: z.string().optional().describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
33
|
+
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
21
34
|
},
|
|
22
|
-
async ({ agent_id, input, pay_with }) => {
|
|
35
|
+
async ({ agent_id, input, pay_with, confirmed }) => {
|
|
23
36
|
if (!hasWalletConfigured()) {
|
|
24
37
|
return text(
|
|
25
38
|
"No wallet configured. Set one up first:\n\n" +
|
|
@@ -28,16 +41,36 @@ export function registerRunTools(server: McpServer): void {
|
|
|
28
41
|
);
|
|
29
42
|
}
|
|
30
43
|
|
|
31
|
-
// Resolve
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
// Resolve agent and fetch details
|
|
45
|
+
let agent: { id: string; name?: string; pricePer1kTokens?: string; successRate?: number };
|
|
46
|
+
try {
|
|
47
|
+
agent = await apiGet<typeof agent>(`/agents/${agent_id}`);
|
|
48
|
+
} catch {
|
|
49
|
+
return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const price = parseFloat(agent.pricePer1kTokens ?? "0.01");
|
|
53
|
+
const agentName = agent.name ?? agent_id;
|
|
54
|
+
|
|
55
|
+
// Confirmation step: show price and wait for confirmed: true
|
|
56
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
57
|
+
pendingRuns.set(agent.id, {
|
|
58
|
+
agent: { id: agent.id, name: agentName, price },
|
|
59
|
+
input,
|
|
60
|
+
method: pay_with,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return text([
|
|
64
|
+
`Ready to run ${agentName}`,
|
|
65
|
+
"",
|
|
66
|
+
` Cost: $${price.toFixed(2)}`,
|
|
67
|
+
` Payment: ${pay_with ?? getConfiguredMethods()[0] ?? "auto"}`,
|
|
68
|
+
"",
|
|
69
|
+
"To proceed, call:",
|
|
70
|
+
` run_agent({ agent_id: "${agent.id}", input: <same>, confirmed: true })`,
|
|
71
|
+
"",
|
|
72
|
+
"To cancel, do nothing.",
|
|
73
|
+
].join("\n"));
|
|
41
74
|
}
|
|
42
75
|
|
|
43
76
|
const method = pay_with;
|
|
@@ -45,7 +78,7 @@ export function registerRunTools(server: McpServer): void {
|
|
|
45
78
|
let result: Record<string, unknown>;
|
|
46
79
|
try {
|
|
47
80
|
result = await apiPostWithPayment<Record<string, unknown>>(
|
|
48
|
-
`/agents/${
|
|
81
|
+
`/agents/${agent.id}/run`,
|
|
49
82
|
{ input: processedInput },
|
|
50
83
|
method,
|
|
51
84
|
);
|
|
@@ -64,36 +97,65 @@ export function registerRunTools(server: McpServer): void {
|
|
|
64
97
|
}
|
|
65
98
|
return text(`Error: ${msg}`);
|
|
66
99
|
}
|
|
100
|
+
|
|
101
|
+
// Clean up pending confirmation
|
|
102
|
+
pendingRuns.delete(agent.id);
|
|
103
|
+
|
|
67
104
|
const formatted = formatRunResult(result, {
|
|
68
105
|
paymentMethod: method ?? getConfiguredMethods()[0],
|
|
69
106
|
});
|
|
70
107
|
const jobId = (result.job_id as string) ?? "";
|
|
71
|
-
const
|
|
108
|
+
const resultAgentId = (result.agent_id as string) ?? agent.id;
|
|
72
109
|
|
|
73
110
|
if (result.feedback_token) {
|
|
74
|
-
storeFeedbackToken(jobId, result.feedback_token as string,
|
|
111
|
+
storeFeedbackToken(jobId, result.feedback_token as string, resultAgentId);
|
|
75
112
|
}
|
|
76
113
|
|
|
114
|
+
const actualCost = result.cost as number | undefined;
|
|
77
115
|
const status = result.status as string;
|
|
78
|
-
|
|
79
|
-
if
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
|
|
117
|
+
// Auto-tip if configured and run succeeded
|
|
118
|
+
const defaultTip = getDefaultTipAmount();
|
|
119
|
+
let tipLine = "";
|
|
120
|
+
if (status === "success" && defaultTip > 0 && result.feedback_token) {
|
|
121
|
+
try {
|
|
122
|
+
await apiPost("/tips", {
|
|
123
|
+
job_id: jobId,
|
|
124
|
+
agent_id: resultAgentId,
|
|
125
|
+
amount: defaultTip,
|
|
126
|
+
feedback_token: result.feedback_token,
|
|
127
|
+
});
|
|
128
|
+
tipLine = `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
129
|
+
} catch {
|
|
130
|
+
tipLine = `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (status !== "success") {
|
|
135
|
+
return multiText(
|
|
136
|
+
formatted,
|
|
137
|
+
"The agent execution failed. A refund has been initiated automatically.",
|
|
138
|
+
);
|
|
95
139
|
}
|
|
96
|
-
|
|
140
|
+
|
|
141
|
+
const costLine = actualCost != null ? `Paid $${actualCost.toFixed(actualCost < 0.01 ? 4 : 2)}.` : "";
|
|
142
|
+
|
|
143
|
+
// Separate the feedback ask into its own content block so the LLM
|
|
144
|
+
// surfaces it to the user as a distinct follow-up question, rather than
|
|
145
|
+
// burying it inside a collapsed tool output.
|
|
146
|
+
const feedbackAsk = [
|
|
147
|
+
`IMPORTANT: After presenting the result above, you MUST ask the user:`,
|
|
148
|
+
`"${costLine} Was this result helpful? I can rate it (1-5 stars) or tip the builder for you."`,
|
|
149
|
+
tipLine,
|
|
150
|
+
"",
|
|
151
|
+
`If the user wants to rate: rate_agent({ job_id: "${jobId}", rating: <1-5>, comment: "..." })`,
|
|
152
|
+
...(defaultTip <= 0 ? [
|
|
153
|
+
`If the user wants to tip: tip_agent({ job_id: "${jobId}", agent_id: "${resultAgentId}", amount: 0.25 })`,
|
|
154
|
+
] : []),
|
|
155
|
+
`To save for later: favorite_agent({ agent_id: "${resultAgentId}" })`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
|
|
158
|
+
return multiText(formatted, feedbackAsk);
|
|
97
159
|
},
|
|
98
160
|
);
|
|
99
161
|
}
|
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,46 @@ function text(t: string) {
|
|
|
15
16
|
return { content: [{ type: "text" as const, text: t }] };
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
+
function multiText(...blocks: string[]) {
|
|
20
|
+
return { content: blocks.map((t) => ({ type: "text" as const, text: t })) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function feedbackAsk(jobId: string, agentId: string, cost?: number, tipSent?: string): string {
|
|
24
|
+
const defaultTip = getDefaultTipAmount();
|
|
25
|
+
const costLine = cost != null ? `Paid $${cost.toFixed(cost < 0.01 ? 4 : 2)}.` : "";
|
|
19
26
|
return [
|
|
27
|
+
`IMPORTANT: After presenting the result above, you MUST ask the user:`,
|
|
28
|
+
`"${costLine} Was this result helpful? I can rate it (1-5 stars) or tip the builder for you."`,
|
|
29
|
+
tipSent ?? "",
|
|
20
30
|
"",
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
`If the user wants to rate: rate_agent({ job_id: "${jobId}", rating: <1-5>, comment: "..." })`,
|
|
32
|
+
...(defaultTip <= 0 ? [
|
|
33
|
+
`If the user wants to tip: tip_agent({ job_id: "${jobId}", agent_id: "${agentId}", amount: 0.25 })`,
|
|
34
|
+
] : []),
|
|
35
|
+
`To save for later: favorite_agent({ agent_id: "${agentId}" })`,
|
|
26
36
|
].join("\n");
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
async function autoTip(jobId: string, agentId: string, agentName: string, feedbackToken: string): Promise<string> {
|
|
40
|
+
const defaultTip = getDefaultTipAmount();
|
|
41
|
+
if (defaultTip <= 0) return "";
|
|
42
|
+
try {
|
|
43
|
+
await apiPost("/tips", {
|
|
44
|
+
job_id: jobId,
|
|
45
|
+
agent_id: agentId,
|
|
46
|
+
amount: defaultTip,
|
|
47
|
+
feedback_token: feedbackToken,
|
|
48
|
+
});
|
|
49
|
+
return `Auto-tipped $${defaultTip.toFixed(2)} to ${agentName}.`;
|
|
50
|
+
} catch {
|
|
51
|
+
return `Auto-tip of $${defaultTip.toFixed(2)} failed.`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
export function registerSolveTools(server: McpServer): void {
|
|
30
56
|
server.tool(
|
|
31
57
|
"solve",
|
|
32
|
-
"Solve a task by finding the best agent, paying, and executing. The primary way to use the marketplace
|
|
58
|
+
"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
59
|
{
|
|
34
60
|
intent: z
|
|
35
61
|
.string()
|
|
@@ -52,8 +78,12 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
52
78
|
.describe(
|
|
53
79
|
"Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted.",
|
|
54
80
|
),
|
|
81
|
+
confirmed: z
|
|
82
|
+
.boolean()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
55
85
|
},
|
|
56
|
-
async ({ intent, input, budget, pay_with }) => {
|
|
86
|
+
async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
57
87
|
if (!hasWalletConfigured()) {
|
|
58
88
|
return text(
|
|
59
89
|
"No wallet configured. Set one up first:\n\n" +
|
|
@@ -74,12 +104,15 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
74
104
|
});
|
|
75
105
|
const jobId = (result as Record<string, unknown>).job_id as string ?? "";
|
|
76
106
|
const agentId = (result as Record<string, unknown>).agent_id as string ?? "";
|
|
107
|
+
const agentName = (result as Record<string, unknown>).agent_name as string ?? "";
|
|
77
108
|
|
|
78
109
|
if (result.feedback_token) {
|
|
79
110
|
storeFeedbackToken(jobId, result.feedback_token as string, agentId);
|
|
80
111
|
}
|
|
81
112
|
|
|
82
|
-
|
|
113
|
+
const cost = (result as Record<string, unknown>).cost as number | undefined;
|
|
114
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token as string) : "";
|
|
115
|
+
return multiText(formatRunResult(result), feedbackAsk(jobId, agentId, cost, tipMsg));
|
|
83
116
|
} catch (err: unknown) {
|
|
84
117
|
const isAuthError =
|
|
85
118
|
err instanceof Error &&
|
|
@@ -113,13 +146,28 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
113
146
|
});
|
|
114
147
|
const selected = affordable[0] ?? agents[0];
|
|
115
148
|
|
|
116
|
-
// Estimate cost for the selected agent
|
|
117
149
|
const selectedPrice = parseFloat(selected.pricePer1kTokens ?? "0.01");
|
|
118
150
|
const estimatedCost =
|
|
119
151
|
selected.pricingModel === "fixed"
|
|
120
152
|
? selectedPrice
|
|
121
153
|
: (inputTokens / 1000) * selectedPrice;
|
|
122
154
|
|
|
155
|
+
// Confirmation step: show discovery + price, wait for confirmed: true
|
|
156
|
+
if (requiresSpendConfirmation() && !confirmed) {
|
|
157
|
+
return text([
|
|
158
|
+
discovery,
|
|
159
|
+
"",
|
|
160
|
+
`Best match: ${selected.name}`,
|
|
161
|
+
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
162
|
+
`Payment: ${method}`,
|
|
163
|
+
"",
|
|
164
|
+
"To proceed, call:",
|
|
165
|
+
` solve({ intent: "${intent}", input: <same>, budget: ${budget}, confirmed: true })`,
|
|
166
|
+
"",
|
|
167
|
+
"To cancel, do nothing.",
|
|
168
|
+
].join("\n"));
|
|
169
|
+
}
|
|
170
|
+
|
|
123
171
|
let result: Record<string, unknown>;
|
|
124
172
|
const processedInput2 = await uploadLocalFiles(input);
|
|
125
173
|
try {
|
|
@@ -140,7 +188,6 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
140
188
|
return text(`Error: ${apiErr?.message ?? "Failed to run agent"}`);
|
|
141
189
|
}
|
|
142
190
|
|
|
143
|
-
// Cache feedback token if present
|
|
144
191
|
const jobId = (result as Record<string, unknown>).job_id as string ?? "";
|
|
145
192
|
const agentId2 = (result as Record<string, unknown>).agent_id as string ?? selected.id;
|
|
146
193
|
|
|
@@ -148,7 +195,9 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
148
195
|
storeFeedbackToken(jobId, result.feedback_token as string, agentId2);
|
|
149
196
|
}
|
|
150
197
|
|
|
151
|
-
|
|
198
|
+
const actualCost = (result as Record<string, unknown>).cost as number | undefined;
|
|
199
|
+
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId2, selected.name ?? "", result.feedback_token as string) : "";
|
|
200
|
+
|
|
152
201
|
const output = [
|
|
153
202
|
discovery,
|
|
154
203
|
"",
|
|
@@ -156,10 +205,9 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
156
205
|
`Estimated cost: $${estimatedCost.toFixed(4)}`,
|
|
157
206
|
"",
|
|
158
207
|
formatRunResult(result, { paymentMethod: method }),
|
|
159
|
-
ratingPrompt(jobId),
|
|
160
208
|
].join("\n");
|
|
161
209
|
|
|
162
|
-
return
|
|
210
|
+
return multiText(output, feedbackAsk(jobId, agentId2, actualCost, tipMsg));
|
|
163
211
|
},
|
|
164
212
|
);
|
|
165
213
|
}
|