@agentwonderland/mcp 0.1.52 → 0.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/__tests__/api-client.test.js +40 -0
- package/dist/core/__tests__/link-cli.test.d.ts +1 -0
- package/dist/core/__tests__/link-cli.test.js +102 -0
- package/dist/core/api-client.js +18 -0
- package/dist/core/link-cli.d.ts +5 -0
- package/dist/core/link-cli.js +25 -25
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +8 -5
- package/dist/tools/__tests__/playbooks.test.d.ts +1 -0
- package/dist/tools/__tests__/playbooks.test.js +2043 -0
- package/dist/tools/__tests__/run.test.js +26 -0
- package/dist/tools/__tests__/wallet.test.js +5 -4
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/passes.js +4 -21
- package/dist/tools/playbooks.d.ts +2 -0
- package/dist/tools/playbooks.js +880 -0
- package/dist/tools/run.js +10 -18
- package/dist/tools/solve.js +12 -19
- package/dist/tools/wallet.js +9 -12
- package/package.json +1 -1
- package/src/core/__tests__/api-client.test.ts +44 -0
- package/src/core/__tests__/link-cli.test.ts +125 -0
- package/src/core/api-client.ts +18 -0
- package/src/core/link-cli.ts +25 -27
- package/src/core/version.ts +1 -1
- package/src/index.ts +8 -5
- package/src/tools/__tests__/playbooks.test.ts +2285 -0
- package/src/tools/__tests__/run.test.ts +33 -0
- package/src/tools/__tests__/wallet.test.ts +5 -4
- package/src/tools/index.ts +1 -0
- package/src/tools/passes.ts +3 -21
- package/src/tools/playbooks.ts +1100 -0
- package/src/tools/run.ts +10 -17
- package/src/tools/solve.ts +11 -18
- package/src/tools/wallet.ts +9 -12
package/dist/tools/run.js
CHANGED
|
@@ -2,12 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
3
3
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
4
4
|
import { formatCreditPackOffer, getActiveCreditPack, getCreditPackInventory, getCreditPackProgram, } from "../core/passes.js";
|
|
5
|
-
import { getCompatiblePaymentMethods, getConfiguredMethods, hasWalletConfigured, getWalletAddress, normalizePaymentMethod,
|
|
5
|
+
import { getCompatiblePaymentMethods, getConfiguredMethods, hasWalletConfigured, getWalletAddress, normalizePaymentMethod, } from "../core/payments.js";
|
|
6
6
|
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
7
7
|
import { formatRunResult } from "../core/formatters.js";
|
|
8
8
|
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
9
9
|
import { storeFeedbackToken } from "./_token-cache.js";
|
|
10
|
-
import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
|
|
11
10
|
import { formatPaymentLabel, formatRunConfirmationCommand, resolveConfirmationMethod, } from "./_payment-confirmation.js";
|
|
12
11
|
const POLL_INTERVAL_MS = 3000;
|
|
13
12
|
const POLL_MAX_MS = 300000;
|
|
@@ -88,30 +87,17 @@ export function registerRunTools(server) {
|
|
|
88
87
|
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. Local file paths in the input (e.g. /Users/.../photo.jpg) are automatically uploaded to temporary storage and replaced with download URLs before execution. If a file you need isn't on this MCP server's filesystem (e.g. a sandboxed /mnt/... attachment), call upload_file first to get a presigned upload URL, PUT the bytes to it, then pass the returned GET URL as input instead — that keeps the bytes out of the conversation context.", {
|
|
89
88
|
agent_id: z.string().describe("Agent ID (UUID, slug, or name)"),
|
|
90
89
|
input: z.record(z.string(), z.unknown()).describe("Input payload for the agent"),
|
|
91
|
-
pay_with: z.string().trim().min(1).optional().describe("
|
|
90
|
+
pay_with: z.string().trim().min(1).optional().describe("Launch payment method — wallet ID or chain name (tempo, base, solana). Auto-detected if omitted."),
|
|
92
91
|
confirmed: z.boolean().optional().describe("Set to true to confirm spending after seeing the price quote."),
|
|
93
92
|
}, async ({ agent_id, input, pay_with, confirmed }) => {
|
|
94
93
|
if (!hasWalletConfigured()) {
|
|
95
|
-
if (isCardPaymentEnabled()) {
|
|
96
|
-
try {
|
|
97
|
-
const { url } = await getOrCreatePendingCardSetup();
|
|
98
|
-
return multiText(...formatCardSetupBlocks(url));
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Fall through to the setup message below.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
94
|
const setupLines = [
|
|
105
95
|
"No payment method configured.",
|
|
106
96
|
"",
|
|
107
|
-
"
|
|
108
|
-
"You can also run wallet_setup({ action: \"start\" }) to see all payment options.",
|
|
97
|
+
"Run wallet_setup({ action: \"start\" }) to configure a launch USDC payment method.",
|
|
109
98
|
"",
|
|
110
|
-
"
|
|
99
|
+
"Launch options: Tempo USDC, Base USDC, or Solana USDC.",
|
|
111
100
|
];
|
|
112
|
-
if (isCardPaymentEnabled()) {
|
|
113
|
-
setupLines.push("", "Or wallet_setup({ action: \"add-card\" }) to connect a credit card.");
|
|
114
|
-
}
|
|
115
101
|
return text(setupLines.join("\n"));
|
|
116
102
|
}
|
|
117
103
|
let agent;
|
|
@@ -166,6 +152,9 @@ export function registerRunTools(server) {
|
|
|
166
152
|
` Cost: $${price.toFixed(2)}`,
|
|
167
153
|
` Payment: ${formatPaymentLabel(method)}`,
|
|
168
154
|
];
|
|
155
|
+
if (method === "link") {
|
|
156
|
+
quoteLines.push(" Link: after confirming here, approve the Link spend request and rerun the confirmed call.");
|
|
157
|
+
}
|
|
169
158
|
const creditPackLines = buildCreditPackOfferLines(agent);
|
|
170
159
|
if (creditPackLines.length > 0) {
|
|
171
160
|
quoteLines.push("", ...creditPackLines);
|
|
@@ -226,6 +215,9 @@ export function registerRunTools(server) {
|
|
|
226
215
|
].join("\n"));
|
|
227
216
|
}
|
|
228
217
|
const msg = apiErr?.message ?? "Failed to run agent";
|
|
218
|
+
if (msg.includes("Link approval required.")) {
|
|
219
|
+
return text(msg);
|
|
220
|
+
}
|
|
229
221
|
if (msg.includes("Missing required field") || msg.includes("validation failed")) {
|
|
230
222
|
return text(`Error: ${msg}\n\nUse get_agent("${agent_id}") to see the required input fields.`);
|
|
231
223
|
}
|
package/dist/tools/solve.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
3
|
-
import {
|
|
4
|
-
import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, isCardPaymentEnabled, } from "../core/payments.js";
|
|
3
|
+
import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, } from "../core/payments.js";
|
|
5
4
|
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
6
5
|
import { agentList, formatRunResult } from "../core/formatters.js";
|
|
7
6
|
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
@@ -126,33 +125,20 @@ export function registerSolveTools(server) {
|
|
|
126
125
|
.trim()
|
|
127
126
|
.min(1)
|
|
128
127
|
.optional()
|
|
129
|
-
.describe("
|
|
128
|
+
.describe("Launch payment method — wallet ID or chain name (tempo, base, solana). Auto-detected if omitted."),
|
|
130
129
|
confirmed: z
|
|
131
130
|
.boolean()
|
|
132
131
|
.optional()
|
|
133
132
|
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
134
133
|
}, async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
135
134
|
if (!hasWalletConfigured()) {
|
|
136
|
-
if (isCardPaymentEnabled()) {
|
|
137
|
-
try {
|
|
138
|
-
const { url } = await getOrCreatePendingCardSetup();
|
|
139
|
-
return multiText(...formatCardSetupBlocks(url));
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// Fall through to the setup message below.
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
135
|
const setupLines = [
|
|
146
136
|
"No payment method configured.",
|
|
147
137
|
"",
|
|
148
|
-
"
|
|
149
|
-
"You can also run wallet_setup({ action: \"start\" }) to see all payment options.",
|
|
138
|
+
"Run wallet_setup({ action: \"start\" }) to configure a launch USDC payment method.",
|
|
150
139
|
"",
|
|
151
|
-
"
|
|
140
|
+
"Launch options: Tempo USDC, Base USDC, or Solana USDC.",
|
|
152
141
|
];
|
|
153
|
-
if (isCardPaymentEnabled()) {
|
|
154
|
-
setupLines.push("", "Or wallet_setup({ action: \"add-card\" }) to connect a credit card.");
|
|
155
|
-
}
|
|
156
142
|
return text(setupLines.join("\n"));
|
|
157
143
|
}
|
|
158
144
|
const pendingKey = makeSolvePendingKey(intent, input, budget);
|
|
@@ -241,6 +227,9 @@ export function registerSolveTools(server) {
|
|
|
241
227
|
`Best match: ${selected.name}`,
|
|
242
228
|
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
243
229
|
`Payment: ${formatPaymentLabel(method)}`,
|
|
230
|
+
...(method === "link"
|
|
231
|
+
? ["Link: after confirming here, approve the Link spend request and rerun the confirmed call."]
|
|
232
|
+
: []),
|
|
244
233
|
...(() => {
|
|
245
234
|
const summary = buildCreditPackSummary(selected);
|
|
246
235
|
return summary.length > 0 ? ["", ...summary] : [];
|
|
@@ -298,7 +287,11 @@ export function registerSolveTools(server) {
|
|
|
298
287
|
})(),
|
|
299
288
|
].join("\n"));
|
|
300
289
|
}
|
|
301
|
-
|
|
290
|
+
const msg = apiErr?.message ?? "Failed to run agent";
|
|
291
|
+
if (msg.includes("Link approval required.")) {
|
|
292
|
+
return text(msg);
|
|
293
|
+
}
|
|
294
|
+
return text(`Error: ${msg}`);
|
|
302
295
|
}
|
|
303
296
|
pendingSolves.delete(pendingKey);
|
|
304
297
|
const jobId = result.job_id ?? "";
|
package/dist/tools/wallet.js
CHANGED
|
@@ -30,21 +30,18 @@ function isFreshLinkSetup(pending) {
|
|
|
30
30
|
}
|
|
31
31
|
function formatPaymentSetupMenu() {
|
|
32
32
|
return [
|
|
33
|
-
"Choose a payment method to set up:",
|
|
33
|
+
"Choose a launch payment method to set up:",
|
|
34
34
|
"",
|
|
35
|
-
"1.
|
|
36
|
-
" wallet_setup({ action: \"add-link\" })",
|
|
37
|
-
"",
|
|
38
|
-
"2. Tempo USDC",
|
|
35
|
+
"1. Tempo USDC",
|
|
39
36
|
" wallet_setup({ action: \"create\", chain: \"tempo\" })",
|
|
40
37
|
"",
|
|
41
|
-
"
|
|
38
|
+
"2. Base USDC",
|
|
42
39
|
" wallet_setup({ action: \"create\", chain: \"base\" })",
|
|
43
40
|
"",
|
|
44
|
-
"
|
|
41
|
+
"3. Solana USDC",
|
|
45
42
|
" wallet_setup({ action: \"create\", chain: \"solana\" })",
|
|
46
43
|
"",
|
|
47
|
-
"
|
|
44
|
+
"4. Import an existing wallet",
|
|
48
45
|
" wallet_setup({ action: \"import\", chain: \"base\", key: \"<private key>\" })",
|
|
49
46
|
].join("\n");
|
|
50
47
|
}
|
|
@@ -53,7 +50,7 @@ function setDefaultCryptoPaymentMethod(chain) {
|
|
|
53
50
|
}
|
|
54
51
|
export function registerWalletTools(server) {
|
|
55
52
|
// ── wallet_status (extracted from check_wallet) ─────────────────
|
|
56
|
-
server.tool("wallet_status", "Check payment readiness. Shows
|
|
53
|
+
server.tool("wallet_status", "Check launch payment readiness. Shows configured wallets, chains, addresses, and USDC balances.", {}, async () => {
|
|
57
54
|
const wallets = getWallets();
|
|
58
55
|
const card = getCardConfig();
|
|
59
56
|
const link = getLinkConfig();
|
|
@@ -133,10 +130,10 @@ export function registerWalletTools(server) {
|
|
|
133
130
|
return text(lines.join("\n"));
|
|
134
131
|
});
|
|
135
132
|
// ── wallet_setup ────────────────────────────────────────────────
|
|
136
|
-
server.tool("wallet_setup", "Set up or manage an Agent Wonderland payment method. Use 'start' for a guided setup menu.
|
|
133
|
+
server.tool("wallet_setup", "Set up or manage an Agent Wonderland launch payment method. Use 'start' for a guided USDC setup menu. 'create' makes a new crypto wallet (encrypted via OWS if available, otherwise plaintext — run 'enable-ows' to upgrade). 'import' takes an existing private key. 'enable-ows' installs the Open Wallet Standard native module for encrypted at-rest storage. Tempo/Base share one EVM key; Solana uses a separate ed25519 key. Card and Link actions are present for non-launch compatibility only and should not be recommended for launch runs. NEVER delete or rotate keys programmatically; direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.", {
|
|
137
134
|
action: z
|
|
138
135
|
.enum(["start", "create", "import", "add-card", "remove-card", "add-link", "remove-link", "enable-ows"])
|
|
139
|
-
.describe("'start' shows the guided
|
|
136
|
+
.describe("'start' shows the guided launch USDC setup menu, 'create' makes a crypto wallet, 'import' imports an existing key, 'enable-ows' installs encrypted key storage; add-card/add-link are non-launch compatibility actions"),
|
|
140
137
|
name: z
|
|
141
138
|
.string()
|
|
142
139
|
.optional()
|
|
@@ -150,7 +147,7 @@ export function registerWalletTools(server) {
|
|
|
150
147
|
link_payment_method_id: z.string().optional()
|
|
151
148
|
.describe("Link payment method ID from `npx @stripe/link-cli payment-methods list`; used with action 'add-link'."),
|
|
152
149
|
link_payment_method: z.string().optional()
|
|
153
|
-
.describe("Friendly Link payment method selector, such as a
|
|
150
|
+
.describe("Friendly Link payment method selector, such as a saved payment method label or last4; used with action 'add-link'."),
|
|
154
151
|
}, async ({ action, name, key, chain, link_payment_method_id, link_payment_method }) => {
|
|
155
152
|
if (action === "start") {
|
|
156
153
|
return text(formatPaymentSetupMenu());
|
package/package.json
CHANGED
|
@@ -86,4 +86,48 @@ describe("api-client headers", () => {
|
|
|
86
86
|
}),
|
|
87
87
|
);
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
it("labels playbook API calls with MCP surface, version, tool, and action headers", async () => {
|
|
91
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ ok: true }), {
|
|
92
|
+
status: 200,
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
}));
|
|
95
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
96
|
+
const { apiGet, apiPost } = await import("../api-client.js");
|
|
97
|
+
|
|
98
|
+
await apiGet("/playbooks?limit=1");
|
|
99
|
+
await apiGet("/playbooks/competitor-ads");
|
|
100
|
+
await apiGet("/playbooks/competitor-ads/versions");
|
|
101
|
+
await apiGet("/playbooks/competitor-ads/versions/1");
|
|
102
|
+
await apiPost("/playbooks/competitor-ads/favorite", {});
|
|
103
|
+
await apiPost("/playbooks/competitor-ads/feedback", { rating: 5 });
|
|
104
|
+
await apiPost("/playbook-quotes", { slug: "competitor-ads", budget: 5 });
|
|
105
|
+
await apiPost("/playbook-runs", { slug: "competitor-ads", budget: 5 });
|
|
106
|
+
await apiGet("/playbook-runs/run-1");
|
|
107
|
+
await apiPost("/playbook-runs/run-1/steps/ads", { status: "running" });
|
|
108
|
+
|
|
109
|
+
const calls = (fetchMock.mock.calls as unknown as Array<[unknown, { headers?: unknown }]>).map(([url, init]) => ({
|
|
110
|
+
url: String(url).replace("https://api.agentwonderland.test", ""),
|
|
111
|
+
headers: init.headers as Record<string, string>,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
for (const call of calls) {
|
|
115
|
+
expect(call.headers).toMatchObject({
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
Accept: "application/json",
|
|
118
|
+
"X-AW-Surface": "mcp",
|
|
119
|
+
"X-AW-MCP-Version": MCP_PACKAGE_VERSION,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
expect(calls[0]).toMatchObject({ url: "/playbooks?limit=1", headers: { "X-AW-MCP-Tool": "search_playbooks", "X-AW-MCP-Action": "search" } });
|
|
123
|
+
expect(calls[1]).toMatchObject({ url: "/playbooks/competitor-ads", headers: { "X-AW-MCP-Tool": "get_playbook", "X-AW-MCP-Action": "view" } });
|
|
124
|
+
expect(calls[2]).toMatchObject({ url: "/playbooks/competitor-ads/versions", headers: { "X-AW-MCP-Tool": "get_playbook", "X-AW-MCP-Action": "view" } });
|
|
125
|
+
expect(calls[3]).toMatchObject({ url: "/playbooks/competitor-ads/versions/1", headers: { "X-AW-MCP-Tool": "get_playbook", "X-AW-MCP-Action": "view" } });
|
|
126
|
+
expect(calls[4]).toMatchObject({ url: "/playbooks/competitor-ads/favorite", headers: { "X-AW-MCP-Tool": "favorite_playbook", "X-AW-MCP-Action": "favorite" } });
|
|
127
|
+
expect(calls[5]).toMatchObject({ url: "/playbooks/competitor-ads/feedback", headers: { "X-AW-MCP-Tool": "rate_playbook", "X-AW-MCP-Action": "feedback" } });
|
|
128
|
+
expect(calls[6]).toMatchObject({ url: "/playbook-quotes", headers: { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "quote" } });
|
|
129
|
+
expect(calls[7]).toMatchObject({ url: "/playbook-runs", headers: { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "execute" } });
|
|
130
|
+
expect(calls[8]).toMatchObject({ url: "/playbook-runs/run-1", headers: { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "poll" } });
|
|
131
|
+
expect(calls[9]).toMatchObject({ url: "/playbook-runs/run-1/steps/ads", headers: { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "execute" } });
|
|
132
|
+
});
|
|
89
133
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PendingLinkSpendRequest } from "../config.js";
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
execCalls,
|
|
6
|
+
outputs,
|
|
7
|
+
state,
|
|
8
|
+
mockExecFile,
|
|
9
|
+
} = vi.hoisted(() => {
|
|
10
|
+
const execCalls: Array<{ file: string; args: string[] }> = [];
|
|
11
|
+
const outputs: unknown[] = [];
|
|
12
|
+
const state: { pending: PendingLinkSpendRequest | null; pendingWrites: Array<PendingLinkSpendRequest | null> } = {
|
|
13
|
+
pending: null,
|
|
14
|
+
pendingWrites: [],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type MockExecFile = ((file: string, args: string[], options: unknown, callback: (err: Error | null, stdout?: string, stderr?: string) => void) => void) & {
|
|
18
|
+
[key: symbol]: unknown;
|
|
19
|
+
};
|
|
20
|
+
const mockExecFile = vi.fn((file: string, args: string[], _options: unknown, callback: (err: Error | null, stdout?: string, stderr?: string) => void) => {
|
|
21
|
+
execCalls.push({ file, args });
|
|
22
|
+
const next = outputs.shift();
|
|
23
|
+
if (next instanceof Error) {
|
|
24
|
+
callback(next);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
callback(null, JSON.stringify(next ?? null), "");
|
|
28
|
+
}) as unknown as MockExecFile;
|
|
29
|
+
mockExecFile[Symbol.for("nodejs.util.promisify.custom")] = async (file: string, args: string[]) => {
|
|
30
|
+
execCalls.push({ file, args });
|
|
31
|
+
const next = outputs.shift();
|
|
32
|
+
if (next instanceof Error) {
|
|
33
|
+
throw next;
|
|
34
|
+
}
|
|
35
|
+
return { stdout: JSON.stringify(next ?? null), stderr: "" };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return { execCalls, outputs, state, mockExecFile };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
vi.mock("node:child_process", () => ({
|
|
42
|
+
execFile: mockExecFile,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
vi.mock("../config.js", () => ({
|
|
46
|
+
getPendingLinkSpendRequest: () => state.pending,
|
|
47
|
+
setPendingLinkSpendRequest: (pending: PendingLinkSpendRequest | null) => {
|
|
48
|
+
state.pending = pending;
|
|
49
|
+
state.pendingWrites.push(pending);
|
|
50
|
+
},
|
|
51
|
+
setLinkCooldown: vi.fn(),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
describe("Link CLI spend requests", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
execCalls.length = 0;
|
|
58
|
+
outputs.length = 0;
|
|
59
|
+
state.pending = null;
|
|
60
|
+
state.pendingWrites = [];
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns an approval-required error immediately instead of blocking on Link approval", async () => {
|
|
64
|
+
outputs.push([
|
|
65
|
+
{
|
|
66
|
+
id: "lsrq_test_123",
|
|
67
|
+
approval_url: "https://link.example/approve/lsrq_test_123",
|
|
68
|
+
status: "pending_approval",
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const { createLinkSharedPaymentToken, LinkApprovalRequiredError } = await import("../link-cli.js");
|
|
73
|
+
|
|
74
|
+
await expect(createLinkSharedPaymentToken({
|
|
75
|
+
amount: "10",
|
|
76
|
+
currency: "usd",
|
|
77
|
+
context: "Agent Wonderland test",
|
|
78
|
+
expiresAt: Math.floor(Date.now() / 1000) + 300,
|
|
79
|
+
networkId: "profile_test",
|
|
80
|
+
paymentMethodId: "csmrpd_test_123",
|
|
81
|
+
})).rejects.toBeInstanceOf(LinkApprovalRequiredError);
|
|
82
|
+
|
|
83
|
+
expect(execCalls).toHaveLength(1);
|
|
84
|
+
expect(execCalls[0]?.args).toContain("create");
|
|
85
|
+
expect(state.pendingWrites[0]).toMatchObject({
|
|
86
|
+
id: "lsrq_test_123",
|
|
87
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
88
|
+
amount: "10",
|
|
89
|
+
currency: "usd",
|
|
90
|
+
networkId: "profile_test",
|
|
91
|
+
paymentMethodId: "csmrpd_test_123",
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("resumes a stored Link spend request and returns the approved shared payment token", async () => {
|
|
96
|
+
const expiresAt = Math.floor(Date.now() / 1000) + 300;
|
|
97
|
+
state.pending = {
|
|
98
|
+
id: "lsrq_test_123",
|
|
99
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
100
|
+
amount: "10",
|
|
101
|
+
currency: "usd",
|
|
102
|
+
context: "Agent Wonderland test",
|
|
103
|
+
expiresAt,
|
|
104
|
+
networkId: "profile_test",
|
|
105
|
+
paymentMethodId: "csmrpd_test_123",
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
};
|
|
108
|
+
outputs.push({ shared_payment_token: "spt_test_123" });
|
|
109
|
+
|
|
110
|
+
const { createLinkSharedPaymentToken } = await import("../link-cli.js");
|
|
111
|
+
|
|
112
|
+
await expect(createLinkSharedPaymentToken({
|
|
113
|
+
amount: "10",
|
|
114
|
+
currency: "usd",
|
|
115
|
+
context: "Agent Wonderland test",
|
|
116
|
+
expiresAt,
|
|
117
|
+
networkId: "profile_test",
|
|
118
|
+
paymentMethodId: "csmrpd_test_123",
|
|
119
|
+
})).resolves.toBe("spt_test_123");
|
|
120
|
+
|
|
121
|
+
expect(execCalls).toHaveLength(1);
|
|
122
|
+
expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
|
|
123
|
+
expect(state.pendingWrites).toEqual([null]);
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/core/api-client.ts
CHANGED
|
@@ -40,6 +40,24 @@ function inferToolHeaders(path: string, method: string): Record<string, string>
|
|
|
40
40
|
if (path.startsWith("/agents?") || path === "/agents" || path === "/agents/search") {
|
|
41
41
|
return { "X-AW-MCP-Tool": "search_agents", "X-AW-MCP-Action": "search" };
|
|
42
42
|
}
|
|
43
|
+
if (path.startsWith("/playbooks?") || path === "/playbooks") {
|
|
44
|
+
return { "X-AW-MCP-Tool": "search_playbooks", "X-AW-MCP-Action": "search" };
|
|
45
|
+
}
|
|
46
|
+
if (/^\/playbooks\/[^/]+(?:\/versions(?:\/[^/]+)?)?$/.test(path)) {
|
|
47
|
+
return { "X-AW-MCP-Tool": "get_playbook", "X-AW-MCP-Action": "view" };
|
|
48
|
+
}
|
|
49
|
+
if (/^\/playbooks\/[^/]+\/favorite$/.test(path)) {
|
|
50
|
+
return { "X-AW-MCP-Tool": "favorite_playbook", "X-AW-MCP-Action": "favorite" };
|
|
51
|
+
}
|
|
52
|
+
if (/^\/playbooks\/[^/]+\/feedback$/.test(path)) {
|
|
53
|
+
return { "X-AW-MCP-Tool": "rate_playbook", "X-AW-MCP-Action": "feedback" };
|
|
54
|
+
}
|
|
55
|
+
if (path === "/playbook-quotes") {
|
|
56
|
+
return { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "quote" };
|
|
57
|
+
}
|
|
58
|
+
if (path === "/playbook-runs" || path.startsWith("/playbook-runs/")) {
|
|
59
|
+
return { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": method === "GET" ? "poll" : "execute" };
|
|
60
|
+
}
|
|
43
61
|
if (/^\/agents\/[^/]+$/.test(path)) {
|
|
44
62
|
return { "X-AW-MCP-Tool": "get_agent", "X-AW-MCP-Action": "view" };
|
|
45
63
|
}
|
package/src/core/link-cli.ts
CHANGED
|
@@ -11,8 +11,23 @@ const execFileAsync = promisify(execFile);
|
|
|
11
11
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
12
12
|
const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
export class LinkApprovalRequiredError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
public readonly spendRequestId: string,
|
|
17
|
+
public readonly approvalUrl?: string,
|
|
18
|
+
) {
|
|
19
|
+
super(formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl));
|
|
20
|
+
this.name = "LinkApprovalRequiredError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatLinkApprovalRequiredMessage(spendRequestId: string, approvalUrl?: string): string {
|
|
25
|
+
return [
|
|
26
|
+
"Link approval required.",
|
|
27
|
+
"The agent has not run yet and no charge has been captured.",
|
|
28
|
+
approvalUrl ? `Approve this spend request in Link: ${approvalUrl}` : `Spend request: ${spendRequestId}`,
|
|
29
|
+
"After approving, rerun the same tool call with confirmed: true.",
|
|
30
|
+
].join("\n");
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export interface LinkCliAuthStatus {
|
|
@@ -264,6 +279,9 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
264
279
|
return spt;
|
|
265
280
|
} catch (err) {
|
|
266
281
|
const message = err instanceof Error ? err.message : String(err);
|
|
282
|
+
if (err instanceof LinkApprovalRequiredError) {
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
267
285
|
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
268
286
|
throw err;
|
|
269
287
|
}
|
|
@@ -344,9 +362,7 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
344
362
|
if (approval.approvalUrl) {
|
|
345
363
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
346
364
|
}
|
|
347
|
-
|
|
348
|
-
setPendingLinkSpendRequest(null);
|
|
349
|
-
return retrievedSpt;
|
|
365
|
+
throw new LinkApprovalRequiredError(approval.id, approval.approvalUrl);
|
|
350
366
|
}
|
|
351
367
|
|
|
352
368
|
{
|
|
@@ -355,32 +371,14 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
355
371
|
}
|
|
356
372
|
|
|
357
373
|
async function retrieveSharedPaymentToken(spendRequestId: string, approvalUrl?: string): Promise<string> {
|
|
358
|
-
|
|
374
|
+
const retrieved = await runLinkCli([
|
|
359
375
|
"spend-request",
|
|
360
376
|
"retrieve",
|
|
361
377
|
spendRequestId,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
"--max-attempts",
|
|
365
|
-
"150",
|
|
366
|
-
]);
|
|
367
|
-
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
368
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
369
|
-
await sleep(2_000);
|
|
370
|
-
retrieved = await runLinkCli([
|
|
371
|
-
"spend-request",
|
|
372
|
-
"retrieve",
|
|
373
|
-
spendRequestId,
|
|
374
|
-
]);
|
|
375
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
376
|
-
}
|
|
378
|
+
], 30_000);
|
|
379
|
+
const retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
377
380
|
if (retrievedSpt) {
|
|
378
381
|
return retrievedSpt;
|
|
379
382
|
}
|
|
380
|
-
throw new
|
|
381
|
-
[
|
|
382
|
-
"Link spend request finished without a shared payment token.",
|
|
383
|
-
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
384
|
-
].filter(Boolean).join("\n"),
|
|
385
|
-
);
|
|
383
|
+
throw new LinkApprovalRequiredError(spendRequestId, approvalUrl);
|
|
386
384
|
}
|
package/src/core/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.54";
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { registerRebateTools } from "./tools/rebates.js";
|
|
|
18
18
|
import { registerUploadTools } from "./tools/upload.js";
|
|
19
19
|
import { registerProbeTools } from "./tools/probe.js";
|
|
20
20
|
import { registerProviderTools } from "./tools/providers.js";
|
|
21
|
+
import { registerPlaybookTools } from "./tools/playbooks.js";
|
|
21
22
|
|
|
22
23
|
// ── Resources ────────────────────────────────────────────────────
|
|
23
24
|
import { registerAgentResources } from "./resources/agents.js";
|
|
@@ -48,16 +49,17 @@ export async function startMcpServer(): Promise<void> {
|
|
|
48
49
|
"WORKFLOW:",
|
|
49
50
|
"1. solve(intent, input, budget) — one call: find best agent + pay + run. Use when the task is clear.",
|
|
50
51
|
" OR: search_agents() → get_agent() to inspect schema → run_agent() with required fields.",
|
|
52
|
+
" For multi-agent workflows, use search_playbooks() → get_playbook() → run_playbook().",
|
|
51
53
|
"2. If the agent returns status 'processing', poll with get_job(). Async runs resolve automatically.",
|
|
52
|
-
"3. After a successful run, rate_agent() and optionally tip_agent() if the result was useful.",
|
|
54
|
+
"3. After a successful agent run, rate_agent() and optionally tip_agent() if the result was useful.",
|
|
55
|
+
" After a successful playbook run, offer favorite_playbook() or rate_playbook() using the returned run_id.",
|
|
53
56
|
"4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
|
|
54
57
|
"",
|
|
55
58
|
"PAYMENT:",
|
|
56
|
-
"- Supported payment methods:
|
|
57
|
-
"- Link payments may ask the user to approve a spend request in Link before the first charge.",
|
|
59
|
+
"- Supported launch payment methods: Tempo USDC, Base USDC, and Solana USDC.",
|
|
58
60
|
"- Tempo and Base share one EVM wallet key. Solana uses a separate ed25519 key. One OWS wallet can manage both.",
|
|
59
61
|
"- If pay_with is omitted, the MCP auto-selects a compatible configured rail. Pass pay_with explicitly",
|
|
60
|
-
" (tempo | base | solana |
|
|
62
|
+
" (tempo | base | solana | wallet-id) for deterministic behavior.",
|
|
61
63
|
"- Payment is automatic: on a 402 challenge the MCP signs on-chain, submits, then retries. Failed runs are refunded.",
|
|
62
64
|
"- If a specific rail fails, surface the real reason — do NOT silently retry with a different method.",
|
|
63
65
|
"- Headless/automation: set wallet_set_policy() to cap max_per_tx and max_per_day so a runaway loop can't drain funds.",
|
|
@@ -73,7 +75,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
73
75
|
"WALLET HYGIENE:",
|
|
74
76
|
"- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
|
|
75
77
|
"- rebate_status shows accrued, pending, paid, and blocked consumer rebates plus recent payout transactions.",
|
|
76
|
-
"- To set up payments: wallet_setup({ action: \"start\" }).
|
|
78
|
+
"- To set up payments: wallet_setup({ action: \"start\" }). Use Tempo, Base, or Solana USDC for launch runs.",
|
|
77
79
|
"- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
|
|
78
80
|
"- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
|
|
79
81
|
].join("\n"),
|
|
@@ -95,6 +97,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
95
97
|
registerUploadTools(server);
|
|
96
98
|
registerProbeTools(server);
|
|
97
99
|
registerProviderTools(server);
|
|
100
|
+
registerPlaybookTools(server);
|
|
98
101
|
|
|
99
102
|
// Register resources
|
|
100
103
|
registerAgentResources(server);
|