@agentwonderland/mcp 0.1.3 → 0.1.5

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