@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.
@@ -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,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 slug)"),
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
- }, async ({ agent_id, input, pay_with }) => {
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 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
- }
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/${resolvedId}/run`, { input: processedInput }, method);
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 agentId = result.agent_id ?? agent_id;
82
+ const resultAgentId = result.agent_id ?? agent.id;
57
83
  if (result.feedback_token) {
58
- storeFeedbackToken(jobId, result.feedback_token, agentId);
84
+ storeFeedbackToken(jobId, result.feedback_token, resultAgentId);
59
85
  }
86
+ const actualCost = result.cost;
60
87
  const status = result.status;
61
- let prompt;
62
- if (status === "success") {
63
- prompt = [
64
- "",
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",
70
- ].join("\n");
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
- else {
73
- prompt = [
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
- return text(formatted + prompt);
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
  }
@@ -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 ratingPrompt(jobId) {
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
- "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",
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 describe what you need and the system handles discovery, selection, payment, and execution.", {
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
- }, async ({ intent, input, budget, pay_with }) => {
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
- return text(formatRunResult(result) + ratingPrompt(jobId));
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
- // Combine discovery + result
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 text(output);
171
+ return multiText(output, feedbackAsk(jobId, agentId2, actualCost, tipMsg));
126
172
  });
127
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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,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 slug)"),
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 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
- }
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/${resolvedId}/run`,
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 agentId = (result.agent_id as string) ?? agent_id;
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, agentId);
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
- let prompt: string;
79
- if (status === "success") {
80
- prompt = [
81
- "",
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",
87
- ].join("\n");
88
- } else {
89
- prompt = [
90
- "",
91
- "---",
92
- "The agent execution failed. A refund has been initiated automatically",
93
- "and will be returned to your wallet.",
94
- ].join("\n");
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
- return text(formatted + prompt);
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
  }
@@ -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 ratingPrompt(jobId: string): string {
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
- "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",
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 describe what you need and the system handles discovery, selection, payment, and execution.",
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
- return text(formatRunResult(result) + ratingPrompt(jobId));
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
- // Combine discovery + result
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 text(output);
210
+ return multiText(output, feedbackAsk(jobId, agentId2, actualCost, tipMsg));
163
211
  },
164
212
  );
165
213
  }