@agentwonderland/mcp 0.1.52 → 0.1.53
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__/link-cli.test.d.ts +1 -0
- package/dist/core/__tests__/link-cli.test.js +102 -0
- package/dist/core/link-cli.d.ts +5 -0
- package/dist/core/link-cli.js +25 -25
- package/dist/tools/__tests__/run.test.js +26 -0
- package/dist/tools/run.js +6 -0
- package/dist/tools/solve.js +8 -1
- package/package.json +1 -1
- package/src/core/__tests__/link-cli.test.ts +125 -0
- package/src/core/link-cli.ts +25 -27
- package/src/tools/__tests__/run.test.ts +33 -0
- package/src/tools/run.ts +7 -0
- package/src/tools/solve.ts +8 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const { execCalls, outputs, state, mockExecFile, } = vi.hoisted(() => {
|
|
3
|
+
const execCalls = [];
|
|
4
|
+
const outputs = [];
|
|
5
|
+
const state = {
|
|
6
|
+
pending: null,
|
|
7
|
+
pendingWrites: [],
|
|
8
|
+
};
|
|
9
|
+
const mockExecFile = vi.fn((file, args, _options, callback) => {
|
|
10
|
+
execCalls.push({ file, args });
|
|
11
|
+
const next = outputs.shift();
|
|
12
|
+
if (next instanceof Error) {
|
|
13
|
+
callback(next);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
callback(null, JSON.stringify(next ?? null), "");
|
|
17
|
+
});
|
|
18
|
+
mockExecFile[Symbol.for("nodejs.util.promisify.custom")] = async (file, args) => {
|
|
19
|
+
execCalls.push({ file, args });
|
|
20
|
+
const next = outputs.shift();
|
|
21
|
+
if (next instanceof Error) {
|
|
22
|
+
throw next;
|
|
23
|
+
}
|
|
24
|
+
return { stdout: JSON.stringify(next ?? null), stderr: "" };
|
|
25
|
+
};
|
|
26
|
+
return { execCalls, outputs, state, mockExecFile };
|
|
27
|
+
});
|
|
28
|
+
vi.mock("node:child_process", () => ({
|
|
29
|
+
execFile: mockExecFile,
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../config.js", () => ({
|
|
32
|
+
getPendingLinkSpendRequest: () => state.pending,
|
|
33
|
+
setPendingLinkSpendRequest: (pending) => {
|
|
34
|
+
state.pending = pending;
|
|
35
|
+
state.pendingWrites.push(pending);
|
|
36
|
+
},
|
|
37
|
+
setLinkCooldown: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
describe("Link CLI spend requests", () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
execCalls.length = 0;
|
|
43
|
+
outputs.length = 0;
|
|
44
|
+
state.pending = null;
|
|
45
|
+
state.pendingWrites = [];
|
|
46
|
+
});
|
|
47
|
+
it("returns an approval-required error immediately instead of blocking on Link approval", async () => {
|
|
48
|
+
outputs.push([
|
|
49
|
+
{
|
|
50
|
+
id: "lsrq_test_123",
|
|
51
|
+
approval_url: "https://link.example/approve/lsrq_test_123",
|
|
52
|
+
status: "pending_approval",
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
const { createLinkSharedPaymentToken, LinkApprovalRequiredError } = await import("../link-cli.js");
|
|
56
|
+
await expect(createLinkSharedPaymentToken({
|
|
57
|
+
amount: "10",
|
|
58
|
+
currency: "usd",
|
|
59
|
+
context: "Agent Wonderland test",
|
|
60
|
+
expiresAt: Math.floor(Date.now() / 1000) + 300,
|
|
61
|
+
networkId: "profile_test",
|
|
62
|
+
paymentMethodId: "csmrpd_test_123",
|
|
63
|
+
})).rejects.toBeInstanceOf(LinkApprovalRequiredError);
|
|
64
|
+
expect(execCalls).toHaveLength(1);
|
|
65
|
+
expect(execCalls[0]?.args).toContain("create");
|
|
66
|
+
expect(state.pendingWrites[0]).toMatchObject({
|
|
67
|
+
id: "lsrq_test_123",
|
|
68
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
69
|
+
amount: "10",
|
|
70
|
+
currency: "usd",
|
|
71
|
+
networkId: "profile_test",
|
|
72
|
+
paymentMethodId: "csmrpd_test_123",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("resumes a stored Link spend request and returns the approved shared payment token", async () => {
|
|
76
|
+
const expiresAt = Math.floor(Date.now() / 1000) + 300;
|
|
77
|
+
state.pending = {
|
|
78
|
+
id: "lsrq_test_123",
|
|
79
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
80
|
+
amount: "10",
|
|
81
|
+
currency: "usd",
|
|
82
|
+
context: "Agent Wonderland test",
|
|
83
|
+
expiresAt,
|
|
84
|
+
networkId: "profile_test",
|
|
85
|
+
paymentMethodId: "csmrpd_test_123",
|
|
86
|
+
createdAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
outputs.push({ shared_payment_token: "spt_test_123" });
|
|
89
|
+
const { createLinkSharedPaymentToken } = await import("../link-cli.js");
|
|
90
|
+
await expect(createLinkSharedPaymentToken({
|
|
91
|
+
amount: "10",
|
|
92
|
+
currency: "usd",
|
|
93
|
+
context: "Agent Wonderland test",
|
|
94
|
+
expiresAt,
|
|
95
|
+
networkId: "profile_test",
|
|
96
|
+
paymentMethodId: "csmrpd_test_123",
|
|
97
|
+
})).resolves.toBe("spt_test_123");
|
|
98
|
+
expect(execCalls).toHaveLength(1);
|
|
99
|
+
expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
|
|
100
|
+
expect(state.pendingWrites).toEqual([null]);
|
|
101
|
+
});
|
|
102
|
+
});
|
package/dist/core/link-cli.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export declare class LinkApprovalRequiredError extends Error {
|
|
2
|
+
readonly spendRequestId: string;
|
|
3
|
+
readonly approvalUrl?: string | undefined;
|
|
4
|
+
constructor(spendRequestId: string, approvalUrl?: string | undefined);
|
|
5
|
+
}
|
|
1
6
|
export interface LinkCliAuthStatus {
|
|
2
7
|
authenticated: boolean;
|
|
3
8
|
credentialsPath?: string;
|
package/dist/core/link-cli.js
CHANGED
|
@@ -4,8 +4,23 @@ import { getPendingLinkSpendRequest, setLinkCooldown, setPendingLinkSpendRequest
|
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
5
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
6
6
|
const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export class LinkApprovalRequiredError extends Error {
|
|
8
|
+
spendRequestId;
|
|
9
|
+
approvalUrl;
|
|
10
|
+
constructor(spendRequestId, approvalUrl) {
|
|
11
|
+
super(formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl));
|
|
12
|
+
this.spendRequestId = spendRequestId;
|
|
13
|
+
this.approvalUrl = approvalUrl;
|
|
14
|
+
this.name = "LinkApprovalRequiredError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function formatLinkApprovalRequiredMessage(spendRequestId, approvalUrl) {
|
|
18
|
+
return [
|
|
19
|
+
"Link approval required.",
|
|
20
|
+
"The agent has not run yet and no charge has been captured.",
|
|
21
|
+
approvalUrl ? `Approve this spend request in Link: ${approvalUrl}` : `Spend request: ${spendRequestId}`,
|
|
22
|
+
"After approving, rerun the same tool call with confirmed: true.",
|
|
23
|
+
].join("\n");
|
|
9
24
|
}
|
|
10
25
|
async function runLinkCli(args, timeout = LINK_CLI_TIMEOUT_MS) {
|
|
11
26
|
try {
|
|
@@ -220,6 +235,9 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
220
235
|
}
|
|
221
236
|
catch (err) {
|
|
222
237
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
if (err instanceof LinkApprovalRequiredError) {
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
223
241
|
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
224
242
|
throw err;
|
|
225
243
|
}
|
|
@@ -293,39 +311,21 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
293
311
|
if (approval.approvalUrl) {
|
|
294
312
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
295
313
|
}
|
|
296
|
-
|
|
297
|
-
setPendingLinkSpendRequest(null);
|
|
298
|
-
return retrievedSpt;
|
|
314
|
+
throw new LinkApprovalRequiredError(approval.id, approval.approvalUrl);
|
|
299
315
|
}
|
|
300
316
|
{
|
|
301
317
|
throw new Error("Link spend request completed without a shared payment token in the CLI response.");
|
|
302
318
|
}
|
|
303
319
|
}
|
|
304
320
|
async function retrieveSharedPaymentToken(spendRequestId, approvalUrl) {
|
|
305
|
-
|
|
321
|
+
const retrieved = await runLinkCli([
|
|
306
322
|
"spend-request",
|
|
307
323
|
"retrieve",
|
|
308
324
|
spendRequestId,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
"--max-attempts",
|
|
312
|
-
"150",
|
|
313
|
-
]);
|
|
314
|
-
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
315
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
316
|
-
await sleep(2_000);
|
|
317
|
-
retrieved = await runLinkCli([
|
|
318
|
-
"spend-request",
|
|
319
|
-
"retrieve",
|
|
320
|
-
spendRequestId,
|
|
321
|
-
]);
|
|
322
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
323
|
-
}
|
|
325
|
+
], 30_000);
|
|
326
|
+
const retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
324
327
|
if (retrievedSpt) {
|
|
325
328
|
return retrievedSpt;
|
|
326
329
|
}
|
|
327
|
-
throw new
|
|
328
|
-
"Link spend request finished without a shared payment token.",
|
|
329
|
-
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
330
|
-
].filter(Boolean).join("\n"));
|
|
330
|
+
throw new LinkApprovalRequiredError(spendRequestId, approvalUrl);
|
|
331
331
|
}
|
|
@@ -146,4 +146,30 @@ describe("run_agent MCP tool", () => {
|
|
|
146
146
|
expect(text).toContain("Paid: $0.01 via card");
|
|
147
147
|
expect(text).toContain("Job ID: job-1");
|
|
148
148
|
});
|
|
149
|
+
it("returns Link approval instructions without wrapping them as an execution error", async () => {
|
|
150
|
+
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
151
|
+
mockGetCompatiblePaymentMethods.mockReturnValue(["link"]);
|
|
152
|
+
mockApiPostWithPayment.mockRejectedValueOnce(new Error([
|
|
153
|
+
"Link approval required.",
|
|
154
|
+
"The agent has not run yet and no charge has been captured.",
|
|
155
|
+
"Approve this spend request in Link: https://link.example/approve/lsrq_test",
|
|
156
|
+
"After approving, rerun the same tool call with confirmed: true.",
|
|
157
|
+
].join("\n")));
|
|
158
|
+
const { registerRunTools } = await import("../run.js");
|
|
159
|
+
const harness = makeServerHarness();
|
|
160
|
+
registerRunTools(harness.server);
|
|
161
|
+
const runAgent = harness.handlers.get("run_agent");
|
|
162
|
+
expect(runAgent).toBeDefined();
|
|
163
|
+
const result = await runAgent({
|
|
164
|
+
agent_id: selectedAgent.id,
|
|
165
|
+
input: { text: "hello", target_language: "es" },
|
|
166
|
+
pay_with: "link",
|
|
167
|
+
confirmed: true,
|
|
168
|
+
});
|
|
169
|
+
const text = flattenToolText(result);
|
|
170
|
+
expect(text).toContain("Link approval required.");
|
|
171
|
+
expect(text).toContain("https://link.example/approve/lsrq_test");
|
|
172
|
+
expect(text).not.toContain("Error: Link approval required.");
|
|
173
|
+
expect(mockRecordSpend).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
149
175
|
});
|
package/dist/tools/run.js
CHANGED
|
@@ -166,6 +166,9 @@ export function registerRunTools(server) {
|
|
|
166
166
|
` Cost: $${price.toFixed(2)}`,
|
|
167
167
|
` Payment: ${formatPaymentLabel(method)}`,
|
|
168
168
|
];
|
|
169
|
+
if (method === "link") {
|
|
170
|
+
quoteLines.push(" Link: after confirming here, approve the Link spend request and rerun the confirmed call.");
|
|
171
|
+
}
|
|
169
172
|
const creditPackLines = buildCreditPackOfferLines(agent);
|
|
170
173
|
if (creditPackLines.length > 0) {
|
|
171
174
|
quoteLines.push("", ...creditPackLines);
|
|
@@ -226,6 +229,9 @@ export function registerRunTools(server) {
|
|
|
226
229
|
].join("\n"));
|
|
227
230
|
}
|
|
228
231
|
const msg = apiErr?.message ?? "Failed to run agent";
|
|
232
|
+
if (msg.includes("Link approval required.")) {
|
|
233
|
+
return text(msg);
|
|
234
|
+
}
|
|
229
235
|
if (msg.includes("Missing required field") || msg.includes("validation failed")) {
|
|
230
236
|
return text(`Error: ${msg}\n\nUse get_agent("${agent_id}") to see the required input fields.`);
|
|
231
237
|
}
|
package/dist/tools/solve.js
CHANGED
|
@@ -241,6 +241,9 @@ export function registerSolveTools(server) {
|
|
|
241
241
|
`Best match: ${selected.name}`,
|
|
242
242
|
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
243
243
|
`Payment: ${formatPaymentLabel(method)}`,
|
|
244
|
+
...(method === "link"
|
|
245
|
+
? ["Link: after confirming here, approve the Link spend request and rerun the confirmed call."]
|
|
246
|
+
: []),
|
|
244
247
|
...(() => {
|
|
245
248
|
const summary = buildCreditPackSummary(selected);
|
|
246
249
|
return summary.length > 0 ? ["", ...summary] : [];
|
|
@@ -298,7 +301,11 @@ export function registerSolveTools(server) {
|
|
|
298
301
|
})(),
|
|
299
302
|
].join("\n"));
|
|
300
303
|
}
|
|
301
|
-
|
|
304
|
+
const msg = apiErr?.message ?? "Failed to run agent";
|
|
305
|
+
if (msg.includes("Link approval required.")) {
|
|
306
|
+
return text(msg);
|
|
307
|
+
}
|
|
308
|
+
return text(`Error: ${msg}`);
|
|
302
309
|
}
|
|
303
310
|
pendingSolves.delete(pendingKey);
|
|
304
311
|
const jobId = result.job_id ?? "";
|
package/package.json
CHANGED
|
@@ -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/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
|
}
|
|
@@ -173,4 +173,37 @@ describe("run_agent MCP tool", () => {
|
|
|
173
173
|
expect(text).toContain("Paid: $0.01 via card");
|
|
174
174
|
expect(text).toContain("Job ID: job-1");
|
|
175
175
|
});
|
|
176
|
+
|
|
177
|
+
it("returns Link approval instructions without wrapping them as an execution error", async () => {
|
|
178
|
+
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
179
|
+
mockGetCompatiblePaymentMethods.mockReturnValue(["link"]);
|
|
180
|
+
mockApiPostWithPayment.mockRejectedValueOnce(
|
|
181
|
+
new Error([
|
|
182
|
+
"Link approval required.",
|
|
183
|
+
"The agent has not run yet and no charge has been captured.",
|
|
184
|
+
"Approve this spend request in Link: https://link.example/approve/lsrq_test",
|
|
185
|
+
"After approving, rerun the same tool call with confirmed: true.",
|
|
186
|
+
].join("\n")),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const { registerRunTools } = await import("../run.js");
|
|
190
|
+
const harness = makeServerHarness();
|
|
191
|
+
registerRunTools(harness.server as never);
|
|
192
|
+
|
|
193
|
+
const runAgent = harness.handlers.get("run_agent");
|
|
194
|
+
expect(runAgent).toBeDefined();
|
|
195
|
+
|
|
196
|
+
const result = await runAgent!({
|
|
197
|
+
agent_id: selectedAgent.id,
|
|
198
|
+
input: { text: "hello", target_language: "es" },
|
|
199
|
+
pay_with: "link",
|
|
200
|
+
confirmed: true,
|
|
201
|
+
});
|
|
202
|
+
const text = flattenToolText(result);
|
|
203
|
+
|
|
204
|
+
expect(text).toContain("Link approval required.");
|
|
205
|
+
expect(text).toContain("https://link.example/approve/lsrq_test");
|
|
206
|
+
expect(text).not.toContain("Error: Link approval required.");
|
|
207
|
+
expect(mockRecordSpend).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
176
209
|
});
|
package/src/tools/run.ts
CHANGED
|
@@ -220,6 +220,10 @@ export function registerRunTools(server: McpServer): void {
|
|
|
220
220
|
` Payment: ${formatPaymentLabel(method)}`,
|
|
221
221
|
];
|
|
222
222
|
|
|
223
|
+
if (method === "link") {
|
|
224
|
+
quoteLines.push(" Link: after confirming here, approve the Link spend request and rerun the confirmed call.");
|
|
225
|
+
}
|
|
226
|
+
|
|
223
227
|
const creditPackLines = buildCreditPackOfferLines(agent);
|
|
224
228
|
if (creditPackLines.length > 0) {
|
|
225
229
|
quoteLines.push("", ...creditPackLines);
|
|
@@ -299,6 +303,9 @@ export function registerRunTools(server: McpServer): void {
|
|
|
299
303
|
);
|
|
300
304
|
}
|
|
301
305
|
const msg = apiErr?.message ?? "Failed to run agent";
|
|
306
|
+
if (msg.includes("Link approval required.")) {
|
|
307
|
+
return text(msg);
|
|
308
|
+
}
|
|
302
309
|
if (msg.includes("Missing required field") || msg.includes("validation failed")) {
|
|
303
310
|
return text(`Error: ${msg}\n\nUse get_agent("${agent_id}") to see the required input fields.`);
|
|
304
311
|
}
|
package/src/tools/solve.ts
CHANGED
|
@@ -292,6 +292,9 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
292
292
|
`Best match: ${selected.name}`,
|
|
293
293
|
`Cost: $${estimatedCost.toFixed(2)}`,
|
|
294
294
|
`Payment: ${formatPaymentLabel(method)}`,
|
|
295
|
+
...(method === "link"
|
|
296
|
+
? ["Link: after confirming here, approve the Link spend request and rerun the confirmed call."]
|
|
297
|
+
: []),
|
|
295
298
|
...(() => {
|
|
296
299
|
const summary = buildCreditPackSummary(selected);
|
|
297
300
|
return summary.length > 0 ? ["", ...summary] : [];
|
|
@@ -361,7 +364,11 @@ export function registerSolveTools(server: McpServer): void {
|
|
|
361
364
|
].join("\n"),
|
|
362
365
|
);
|
|
363
366
|
}
|
|
364
|
-
|
|
367
|
+
const msg = apiErr?.message ?? "Failed to run agent";
|
|
368
|
+
if (msg.includes("Link approval required.")) {
|
|
369
|
+
return text(msg);
|
|
370
|
+
}
|
|
371
|
+
return text(`Error: ${msg}`);
|
|
365
372
|
}
|
|
366
373
|
|
|
367
374
|
pendingSolves.delete(pendingKey);
|