@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.
@@ -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
  */
@@ -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: raw.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.
@@ -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
- const parts = [
119
- `${status} ${agent}`,
120
- costStr ? `cost: ${costStr}` : "",
121
- method ? `via ${method}` : "",
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(`job: ${result.job_id}`);
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 slug)"),
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
- }, async ({ agent_id, input, pay_with }) => {
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 slug to UUID if needed (slugs don't contain hyphens in UUID positions)
22
- let resolvedId = agent_id;
23
- const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(agent_id);
24
- if (!isUuid) {
25
- try {
26
- const agent = await apiGet(`/agents/${agent_id}`);
27
- resolvedId = agent.id;
28
- }
29
- catch {
30
- return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
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/${resolvedId}/run`, { input: processedInput }, method);
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 agentId = result.agent_id ?? agent_id;
79
+ const resultAgentId = result.agent_id ?? agent.id;
57
80
  if (result.feedback_token) {
58
- storeFeedbackToken(jobId, result.feedback_token, agentId);
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
- "How was this result? You can:",
67
- ` • rate_agent with job_id "${jobId}" and a score (1-5) — within 1 hour`,
68
- " • tip_agent to show appreciation — within 1 hour",
69
- " favorite_agent to save this agent for later",
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. A refund has been initiated automatically",
77
- "and will be returned to your wallet.",
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);
@@ -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 ratingPrompt(jobId) {
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
- "How was this result? You can:",
15
- ` • rate_agent with job_id "${jobId}" and a score (1-5) — within 1 hour`,
16
- " • tip_agent to show appreciation — within 1 hour",
17
- " favorite_agent to save this agent for later",
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 describe what you need and the system handles discovery, selection, payment, and execution.", {
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
- }, async ({ intent, input, budget, pay_with }) => {
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
- return text(formatRunResult(result) + ratingPrompt(jobId));
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
- // Combine discovery + result
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
- ratingPrompt(jobId),
174
+ feedbackPrompt(jobId, agentId2, selected.name ?? "", actualCost, tipMsg),
124
175
  ].join("\n");
125
176
  return text(output);
126
177
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -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: (raw as Record<string, unknown>).favorites as string[] ?? [],
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
  /**
@@ -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
- const parts = [
156
- `${status} ${agent}`,
157
- costStr ? `cost: ${costStr}` : "",
158
- method ? `via ${method}` : "",
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(`job: ${result.job_id}`);
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 slug)"),
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 slug to UUID if needed (slugs don't contain hyphens in UUID positions)
32
- let resolvedId = agent_id;
33
- const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(agent_id);
34
- if (!isUuid) {
35
- try {
36
- const agent = await apiGet<{ id: string }>(`/agents/${agent_id}`);
37
- resolvedId = agent.id;
38
- } catch {
39
- return text(`Agent "${agent_id}" not found. Use search_agents to find available agents.`);
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/${resolvedId}/run`,
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 agentId = (result.agent_id as string) ?? agent_id;
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, agentId);
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
- "How was this result? You can:",
84
- ` • rate_agent with job_id "${jobId}" and a score (1-5) — within 1 hour`,
85
- " • tip_agent to show appreciation — within 1 hour",
86
- " favorite_agent to save this agent for later",
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. A refund has been initiated automatically",
93
- "and will be returned to your wallet.",
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);
@@ -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 ratingPrompt(jobId: string): string {
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
- "How was this result? You can:",
23
- ` • rate_agent with job_id "${jobId}" and a score (1-5) — within 1 hour`,
24
- " • tip_agent to show appreciation — within 1 hour",
25
- " favorite_agent to save this agent for later",
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 describe what you need and the system handles discovery, selection, payment, and execution.",
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
- return text(formatRunResult(result) + ratingPrompt(jobId));
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
- // Combine discovery + result
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
- ratingPrompt(jobId),
211
+ feedbackPrompt(jobId, agentId2, selected.name ?? "", actualCost, tipMsg),
160
212
  ].join("\n");
161
213
 
162
214
  return text(output);