@agentwonderland/mcp 0.1.54 → 0.1.56
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 +25 -1
- package/dist/core/__tests__/link-cli.test.js +95 -0
- package/dist/core/api-client.d.ts +3 -1
- package/dist/core/api-client.js +21 -2
- package/dist/core/link-cli.d.ts +17 -0
- package/dist/core/link-cli.js +165 -0
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +0 -4
- package/dist/tools/__tests__/playbooks.test.js +114 -34
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/dist/tools/playbooks.js +146 -42
- package/package.json +1 -1
- package/src/core/__tests__/api-client.test.ts +33 -0
- package/src/core/__tests__/link-cli.test.ts +110 -0
- package/src/core/api-client.ts +25 -1
- package/src/core/link-cli.ts +189 -0
- package/src/core/version.ts +1 -1
- package/src/index.ts +0 -4
- package/src/tools/__tests__/playbooks.test.ts +125 -46
- package/src/tools/index.ts +0 -1
- package/src/tools/playbooks.ts +177 -56
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
const mockApiGet = vi.fn();
|
|
3
3
|
const mockApiPost = vi.fn();
|
|
4
|
+
const mockApiPostWithApprovedLinkSpendRequest = vi.fn();
|
|
4
5
|
const mockApiPostWithPayment = vi.fn();
|
|
5
6
|
const mockUploadLocalFiles = vi.fn();
|
|
6
7
|
const mockHasWalletConfigured = vi.fn();
|
|
7
8
|
const mockGetConfiguredMethods = vi.fn();
|
|
8
9
|
const mockGetWalletAddress = vi.fn();
|
|
10
|
+
const mockGetLinkConfig = vi.fn();
|
|
11
|
+
const mockSetPendingLinkSpendRequest = vi.fn();
|
|
9
12
|
const mockRequiresSpendConfirmation = vi.fn();
|
|
10
13
|
const mockCanSpend = vi.fn();
|
|
11
14
|
const mockRecordSpend = vi.fn();
|
|
12
15
|
const mockRequiresPolicyConfirmation = vi.fn();
|
|
13
16
|
const mockStoreFeedbackToken = vi.fn();
|
|
17
|
+
const mockDecodeMppNetworkId = vi.fn();
|
|
18
|
+
const mockEnsureApprovedLinkSpendRequest = vi.fn();
|
|
19
|
+
class MockLinkApprovalRequiredError extends Error {
|
|
20
|
+
approvalUrl;
|
|
21
|
+
spendRequestId;
|
|
22
|
+
constructor(approvalUrl, spendRequestId = "lsrq_test_123") {
|
|
23
|
+
super(`Link approval required: ${approvalUrl}`);
|
|
24
|
+
this.name = "LinkApprovalRequiredError";
|
|
25
|
+
this.approvalUrl = approvalUrl;
|
|
26
|
+
this.spendRequestId = spendRequestId;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
14
29
|
vi.mock("../../core/api-client.js", () => ({
|
|
15
30
|
apiGet: mockApiGet,
|
|
16
31
|
apiPost: mockApiPost,
|
|
32
|
+
apiPostWithApprovedLinkSpendRequest: mockApiPostWithApprovedLinkSpendRequest,
|
|
17
33
|
apiPostWithPayment: mockApiPostWithPayment,
|
|
18
34
|
}));
|
|
19
35
|
vi.mock("../../core/file-upload.js", () => ({
|
|
@@ -25,6 +41,8 @@ vi.mock("../../core/payments.js", () => ({
|
|
|
25
41
|
getWalletAddress: mockGetWalletAddress,
|
|
26
42
|
}));
|
|
27
43
|
vi.mock("../../core/config.js", () => ({
|
|
44
|
+
getLinkConfig: mockGetLinkConfig,
|
|
45
|
+
setPendingLinkSpendRequest: mockSetPendingLinkSpendRequest,
|
|
28
46
|
requiresSpendConfirmation: mockRequiresSpendConfirmation,
|
|
29
47
|
}));
|
|
30
48
|
vi.mock("../../core/spend-policy.js", () => ({
|
|
@@ -35,6 +53,11 @@ vi.mock("../../core/spend-policy.js", () => ({
|
|
|
35
53
|
vi.mock("../_token-cache.js", () => ({
|
|
36
54
|
storeFeedbackToken: mockStoreFeedbackToken,
|
|
37
55
|
}));
|
|
56
|
+
vi.mock("../../core/link-cli.js", () => ({
|
|
57
|
+
decodeMppNetworkId: mockDecodeMppNetworkId,
|
|
58
|
+
ensureApprovedLinkSpendRequest: mockEnsureApprovedLinkSpendRequest,
|
|
59
|
+
LinkApprovalRequiredError: MockLinkApprovalRequiredError,
|
|
60
|
+
}));
|
|
38
61
|
function flattenToolText(result) {
|
|
39
62
|
const content = result?.content ?? [];
|
|
40
63
|
return content
|
|
@@ -105,6 +128,11 @@ describe("playbook MCP tools", () => {
|
|
|
105
128
|
mockHasWalletConfigured.mockReturnValue(true);
|
|
106
129
|
mockGetConfiguredMethods.mockReturnValue(["card"]);
|
|
107
130
|
mockGetWalletAddress.mockResolvedValue("0xabc");
|
|
131
|
+
mockGetLinkConfig.mockReturnValue({
|
|
132
|
+
paymentMethodId: "csmrpd_test_123",
|
|
133
|
+
});
|
|
134
|
+
mockDecodeMppNetworkId.mockResolvedValue("profile_test");
|
|
135
|
+
mockEnsureApprovedLinkSpendRequest.mockResolvedValue("lsrq_test_123");
|
|
108
136
|
mockRequiresSpendConfirmation.mockReturnValue(true);
|
|
109
137
|
mockRequiresPolicyConfirmation.mockReturnValue(false);
|
|
110
138
|
mockCanSpend.mockReturnValue({ ok: true, message: "" });
|
|
@@ -229,41 +257,13 @@ describe("playbook MCP tools", () => {
|
|
|
229
257
|
expect(mockApiPost).toHaveBeenCalledWith("/playbooks/competitor-ads/favorite", {}, { ensureConsumerPrincipal: true });
|
|
230
258
|
expect(text).toContain("Saved competitor-ads to favorites");
|
|
231
259
|
});
|
|
232
|
-
it("
|
|
233
|
-
mockApiPost.mockResolvedValueOnce({
|
|
234
|
-
ok: true,
|
|
235
|
-
feedback_id: "feedback-row-1",
|
|
236
|
-
playbook_slug: "competitor-ads",
|
|
237
|
-
});
|
|
238
|
-
const { registerPlaybookTools } = await import("../playbooks.js");
|
|
239
|
-
const harness = makeServerHarness();
|
|
240
|
-
registerPlaybookTools(harness.server);
|
|
241
|
-
const result = await harness.handlers.get("rate_playbook")({
|
|
242
|
-
slug: "competitor-ads",
|
|
243
|
-
run_id: "22222222-2222-4222-8222-222222222222",
|
|
244
|
-
rating: 5,
|
|
245
|
-
useful: true,
|
|
246
|
-
comment: "Useful.",
|
|
247
|
-
});
|
|
248
|
-
const text = flattenToolText(result);
|
|
249
|
-
expect(mockApiPost).toHaveBeenCalledWith("/playbooks/competitor-ads/feedback", {
|
|
250
|
-
run_id: "22222222-2222-4222-8222-222222222222",
|
|
251
|
-
rating: 5,
|
|
252
|
-
useful: true,
|
|
253
|
-
comment: "Useful.",
|
|
254
|
-
}, { ensureConsumerPrincipal: true });
|
|
255
|
-
expect(text).toContain("Recorded feedback for competitor-ads");
|
|
256
|
-
});
|
|
257
|
-
it("validates favorite and feedback inputs before calling the gateway", async () => {
|
|
260
|
+
it("does not expose per-playbook feedback as an MCP tool", async () => {
|
|
258
261
|
const { registerPlaybookTools } = await import("../playbooks.js");
|
|
259
262
|
const harness = makeServerHarness();
|
|
260
263
|
registerPlaybookTools(harness.server);
|
|
261
264
|
const favoriteResult = await harness.handlers.get("favorite_playbook")({});
|
|
262
|
-
const emptyFeedbackResult = await harness.handlers.get("rate_playbook")({ slug: "competitor-ads" });
|
|
263
|
-
const missingIdFeedbackResult = await harness.handlers.get("rate_playbook")({ rating: 5 });
|
|
264
265
|
expect(flattenToolText(favoriteResult)).toContain("Provide slug or playbook_id.");
|
|
265
|
-
expect(
|
|
266
|
-
expect(flattenToolText(missingIdFeedbackResult)).toContain("Provide slug or playbook_id.");
|
|
266
|
+
expect(harness.handlers.has("rate_playbook")).toBe(false);
|
|
267
267
|
expect(mockApiPost).not.toHaveBeenCalled();
|
|
268
268
|
});
|
|
269
269
|
it("quotes before confirmed execution", async () => {
|
|
@@ -526,12 +526,92 @@ describe("playbook MCP tools", () => {
|
|
|
526
526
|
error_code: null,
|
|
527
527
|
failure_message: null,
|
|
528
528
|
}));
|
|
529
|
-
expect(mockStoreFeedbackToken).
|
|
529
|
+
expect(mockStoreFeedbackToken).not.toHaveBeenCalled();
|
|
530
530
|
expect(text).toContain("Playbook run 22222222-2222-4222-8222-222222222222");
|
|
531
531
|
expect(text).toContain("Charged: $1.25");
|
|
532
|
-
expect(text).toContain("Was this playbook useful?");
|
|
533
|
-
expect(text).toContain("favorite_playbook({ slug: \"competitor-ads\" })");
|
|
534
|
-
expect(text).toContain("rate_playbook
|
|
532
|
+
expect(text).not.toContain("Was this playbook useful?");
|
|
533
|
+
expect(text).not.toContain("favorite_playbook({ slug: \"competitor-ads\" })");
|
|
534
|
+
expect(text).not.toContain("rate_playbook");
|
|
535
|
+
});
|
|
536
|
+
it("uses one approved Link spend request across paid playbook child steps", async () => {
|
|
537
|
+
mockRequiresSpendConfirmation.mockReturnValue(false);
|
|
538
|
+
mockGetConfiguredMethods.mockReturnValue(["link"]);
|
|
539
|
+
mockApiGet
|
|
540
|
+
.mockResolvedValueOnce(playbook)
|
|
541
|
+
.mockResolvedValueOnce({
|
|
542
|
+
run_id: "22222222-2222-4222-8222-222222222222",
|
|
543
|
+
status: "completed",
|
|
544
|
+
playbook_id: playbook.id,
|
|
545
|
+
playbook_slug: playbook.slug,
|
|
546
|
+
playbook_version: 1,
|
|
547
|
+
budget_usd: 5,
|
|
548
|
+
quoted_cost_usd: 1.25,
|
|
549
|
+
charged_usd: 1.25,
|
|
550
|
+
refunded_usd: 0,
|
|
551
|
+
remaining_budget_usd: 3.75,
|
|
552
|
+
steps: [{
|
|
553
|
+
playbook_step_id: "ads",
|
|
554
|
+
step_index: 0,
|
|
555
|
+
node_type: "aw_agent",
|
|
556
|
+
agent_slug: "ad-strategy-intel",
|
|
557
|
+
agent_id: "11111111-1111-4111-8111-111111111111",
|
|
558
|
+
provider_id: null,
|
|
559
|
+
job_id: "33333333-3333-4333-8333-333333333333",
|
|
560
|
+
status: "succeeded",
|
|
561
|
+
quoted_cost_usd: 1.25,
|
|
562
|
+
charged_usd: 1.25,
|
|
563
|
+
refunded_usd: 0,
|
|
564
|
+
}],
|
|
565
|
+
});
|
|
566
|
+
mockApiPost.mockImplementation(async (path) => {
|
|
567
|
+
if (path === "/playbook-runs") {
|
|
568
|
+
return {
|
|
569
|
+
run_id: "22222222-2222-4222-8222-222222222222",
|
|
570
|
+
steps: playbook.current_quote.steps,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (path === "/agents/11111111-1111-4111-8111-111111111111/run") {
|
|
574
|
+
throw {
|
|
575
|
+
status: 402,
|
|
576
|
+
message: "payment required",
|
|
577
|
+
headers: new Headers({ "www-authenticate": "Payment challenge" }),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return { ok: true };
|
|
581
|
+
});
|
|
582
|
+
mockApiPostWithApprovedLinkSpendRequest.mockResolvedValueOnce({
|
|
583
|
+
status: "success",
|
|
584
|
+
job_id: "33333333-3333-4333-8333-333333333333",
|
|
585
|
+
agent_id: "11111111-1111-4111-8111-111111111111",
|
|
586
|
+
output: { rows: 3 },
|
|
587
|
+
cost: 1.25,
|
|
588
|
+
});
|
|
589
|
+
const { registerPlaybookTools } = await import("../playbooks.js");
|
|
590
|
+
const harness = makeServerHarness();
|
|
591
|
+
registerPlaybookTools(harness.server);
|
|
592
|
+
const result = await harness.handlers.get("run_playbook")({
|
|
593
|
+
slug: "competitor-ads",
|
|
594
|
+
input: { domain: "notion.so" },
|
|
595
|
+
budget: 5,
|
|
596
|
+
pay_with: "link",
|
|
597
|
+
confirmed: true,
|
|
598
|
+
});
|
|
599
|
+
const text = flattenToolText(result);
|
|
600
|
+
expect(mockDecodeMppNetworkId).toHaveBeenCalledWith("Payment challenge");
|
|
601
|
+
expect(mockEnsureApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
602
|
+
amount: "500",
|
|
603
|
+
currency: "usd",
|
|
604
|
+
networkId: "profile_test",
|
|
605
|
+
paymentMethodId: "csmrpd_test_123",
|
|
606
|
+
}));
|
|
607
|
+
expect(mockApiPostWithApprovedLinkSpendRequest).toHaveBeenCalledWith("/agents/11111111-1111-4111-8111-111111111111/run", expect.objectContaining({
|
|
608
|
+
input: expect.objectContaining({
|
|
609
|
+
startUrls: [{ url: "https://notion.so" }],
|
|
610
|
+
}),
|
|
611
|
+
}), "lsrq_test_123");
|
|
612
|
+
expect(mockApiPostWithPayment).not.toHaveBeenCalled();
|
|
613
|
+
expect(mockSetPendingLinkSpendRequest).toHaveBeenCalledWith(null);
|
|
614
|
+
expect(text).toContain("Playbook run 22222222-2222-4222-8222-222222222222");
|
|
535
615
|
});
|
|
536
616
|
it("uploads local file inputs before run creation and child execution", async () => {
|
|
537
617
|
mockRequiresSpendConfirmation.mockReturnValue(false);
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -12,4 +12,3 @@ export { registerRebateTools } from "./rebates.js";
|
|
|
12
12
|
export { registerUploadTools } from "./upload.js";
|
|
13
13
|
export { registerProbeTools } from "./probe.js";
|
|
14
14
|
export { registerProviderTools } from "./providers.js";
|
|
15
|
-
export { registerPlaybookTools } from "./playbooks.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -12,4 +12,3 @@ export { registerRebateTools } from "./rebates.js";
|
|
|
12
12
|
export { registerUploadTools } from "./upload.js";
|
|
13
13
|
export { registerProbeTools } from "./probe.js";
|
|
14
14
|
export { registerProviderTools } from "./providers.js";
|
|
15
|
-
export { registerPlaybookTools } from "./playbooks.js";
|
package/dist/tools/playbooks.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
2
|
+
import { apiGet, apiPost, apiPostWithApprovedLinkSpendRequest, apiPostWithPayment } from "../core/api-client.js";
|
|
3
3
|
import { uploadLocalFiles } from "../core/file-upload.js";
|
|
4
4
|
import { hasWalletConfigured, getConfiguredMethods, getWalletAddress } from "../core/payments.js";
|
|
5
|
-
import { requiresSpendConfirmation } from "../core/config.js";
|
|
5
|
+
import { getLinkConfig, setPendingLinkSpendRequest, requiresSpendConfirmation } from "../core/config.js";
|
|
6
6
|
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
7
7
|
import { formatRunResult } from "../core/formatters.js";
|
|
8
|
-
import {
|
|
8
|
+
import { decodeMppNetworkId, ensureApprovedLinkSpendRequest, LinkApprovalRequiredError, } from "../core/link-cli.js";
|
|
9
9
|
import { formatPaymentLabel, resolveConfirmationMethod, } from "./_payment-confirmation.js";
|
|
10
10
|
const POLL_INTERVAL_MS = 3000;
|
|
11
11
|
const POLL_MAX_MS = 300000;
|
|
@@ -19,6 +19,25 @@ function multiText(...blocks) {
|
|
|
19
19
|
function money(value) {
|
|
20
20
|
return `$${value.toFixed(value < 1 ? 4 : 2)}`;
|
|
21
21
|
}
|
|
22
|
+
function cents(valueUsd) {
|
|
23
|
+
return String(Math.max(1, Math.ceil(valueUsd * 100)));
|
|
24
|
+
}
|
|
25
|
+
function nonEmptyLimits(limits) {
|
|
26
|
+
return limits && Object.keys(limits).length > 0 ? limits : undefined;
|
|
27
|
+
}
|
|
28
|
+
function appliedPlaybookLimits(playbook) {
|
|
29
|
+
return nonEmptyLimits(playbook.current_quote.applied_limits ?? playbook.applied_limits ?? playbook.default_limits);
|
|
30
|
+
}
|
|
31
|
+
function fanoutControls(playbook) {
|
|
32
|
+
return playbook.current_quote.fanout_controls ?? playbook.fanout_controls ?? [];
|
|
33
|
+
}
|
|
34
|
+
function buildPlaybookLinkApprovalContext(params) {
|
|
35
|
+
return [
|
|
36
|
+
`Approve up to ${money(params.budget)} for the Agent Wonderland "${params.playbook.name}" playbook.`,
|
|
37
|
+
`This playbook run ${params.runId} has ${params.stepCount} child-agent steps and will stop before exceeding the approved budget.`,
|
|
38
|
+
"Each child agent is still charged at its exact quoted price, unused budget is not spent, and failed child runs keep the existing Agent Wonderland refund behavior.",
|
|
39
|
+
].join(" ");
|
|
40
|
+
}
|
|
22
41
|
async function collectWalletAddresses(paymentMethod) {
|
|
23
42
|
const addresses = new Set();
|
|
24
43
|
const primary = await getWalletAddress(paymentMethod);
|
|
@@ -65,10 +84,12 @@ function formatPlaybookList(playbooks, query) {
|
|
|
65
84
|
}
|
|
66
85
|
const lines = playbooks.map((playbook) => {
|
|
67
86
|
const support = playbook.support_status === "supported" ? "supported" : `catalog-only: ${playbook.support_note ?? "not launch executable"}`;
|
|
87
|
+
const limits = appliedPlaybookLimits(playbook);
|
|
88
|
+
const fanout = limits ? ` · default limits ${JSON.stringify(limits)}` : "";
|
|
68
89
|
return [
|
|
69
90
|
` ${playbook.name} (${playbook.slug})`,
|
|
70
91
|
` ${playbook.outcome}`,
|
|
71
|
-
` ${playbook.current_quote.step_count} step${playbook.current_quote.step_count === 1 ? "" : "s"} · ${money(playbook.current_quote.estimated_cost_usd)} est. · ${support}`,
|
|
92
|
+
` ${playbook.current_quote.step_count} step${playbook.current_quote.step_count === 1 ? "" : "s"} · ${money(playbook.current_quote.estimated_cost_usd)} est. · ${support}${fanout}`,
|
|
72
93
|
` Favorites: ${playbook.stats?.favorite_count ?? 0} · Rating: ${playbook.stats?.average_rating ? `${playbook.stats.average_rating}/5` : "unrated"} (${playbook.stats?.rating_count ?? 0})`,
|
|
73
94
|
` Inspect: get_playbook({ slug: "${playbook.slug}" })`,
|
|
74
95
|
].join("\n");
|
|
@@ -108,9 +129,21 @@ function formatPlaybook(playbook) {
|
|
|
108
129
|
}
|
|
109
130
|
if (playbook.budget_notes.length)
|
|
110
131
|
lines.push("", "Budget notes:", ...playbook.budget_notes.map((note) => ` - ${note}`));
|
|
132
|
+
const limits = appliedPlaybookLimits(playbook);
|
|
133
|
+
const controls = fanoutControls(playbook);
|
|
134
|
+
if (controls.length) {
|
|
135
|
+
lines.push("", "Fanout controls:");
|
|
136
|
+
for (const control of controls) {
|
|
137
|
+
const current = limits?.[control.key] ?? control.default;
|
|
138
|
+
lines.push(` - ${control.key}: default ${control.default}, current ${current}, max ${control.max} — ${control.description}`);
|
|
139
|
+
}
|
|
140
|
+
if (limits)
|
|
141
|
+
lines.push(` Override by passing limits, for example limits: ${JSON.stringify(limits)}`);
|
|
142
|
+
}
|
|
111
143
|
if (playbook.risks.length)
|
|
112
144
|
lines.push("", "Risks:", ...playbook.risks.map((risk) => ` - ${risk}`));
|
|
113
|
-
|
|
145
|
+
const runLimits = formatLimitsField(limits);
|
|
146
|
+
lines.push("", "Sample input:", JSON.stringify(playbook.sample_input, null, 2), "", `Run quote: run_playbook({ slug: "${playbook.slug}", input: <input>, budget: ${Math.max(1, Math.ceil(q.estimated_cost_usd + 1))}${runLimits ? `, ${runLimits}` : ""} })`);
|
|
114
147
|
return lines.join("\n");
|
|
115
148
|
}
|
|
116
149
|
function formatLimitsField(limits) {
|
|
@@ -131,6 +164,18 @@ function formatRunConfirmationCommand(slug, method, budget, limits) {
|
|
|
131
164
|
}
|
|
132
165
|
function formatQuoteNotes(playbook) {
|
|
133
166
|
const lines = [];
|
|
167
|
+
const controls = fanoutControls(playbook);
|
|
168
|
+
if (controls.length > 0) {
|
|
169
|
+
const limits = appliedPlaybookLimits(playbook) ?? {};
|
|
170
|
+
lines.push("", "Fanout controls:");
|
|
171
|
+
for (const control of controls) {
|
|
172
|
+
const current = limits[control.key] ?? control.default;
|
|
173
|
+
lines.push(` - ${control.key}: current ${current}, default ${control.default}, max ${control.max}`);
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(limits).length > 0) {
|
|
176
|
+
lines.push(` Override by editing limits in the confirmation call: ${JSON.stringify(limits)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
134
179
|
if (playbook.budget_notes.length > 0) {
|
|
135
180
|
lines.push("", "Budget and fanout notes:", ...playbook.budget_notes.map((note) => ` - ${note}`));
|
|
136
181
|
}
|
|
@@ -223,6 +268,39 @@ function stepInput(baseInput, step, iteration) {
|
|
|
223
268
|
if (step.agent_slug === "scan-contract-for-risks" && !("file" in input) && input.contract) {
|
|
224
269
|
input.file = input.contract;
|
|
225
270
|
}
|
|
271
|
+
if (step.agent_slug === "marketing-copy-council" && !("brief" in input)) {
|
|
272
|
+
input.brief = input.product ?? input.context ?? input.text;
|
|
273
|
+
}
|
|
274
|
+
if (step.agent_slug === "write-landing-page-copy") {
|
|
275
|
+
const previousOutputs = Array.isArray(input.previous_outputs) ? input.previous_outputs : [];
|
|
276
|
+
const councilOutput = previousOutputs.find((item) => {
|
|
277
|
+
const record = item && typeof item === "object" ? item : null;
|
|
278
|
+
return record?.step === "council" || record?.step === "marketing-copy-council";
|
|
279
|
+
}) ?? previousOutputs[previousOutputs.length - 1];
|
|
280
|
+
const context = {
|
|
281
|
+
brief: input.brief,
|
|
282
|
+
product_slug: input.product_slug,
|
|
283
|
+
council_output: councilOutput,
|
|
284
|
+
previous_outputs: previousOutputs,
|
|
285
|
+
};
|
|
286
|
+
input.product = input.product ?? input.product_name ?? input.product_slug ?? "Landing page";
|
|
287
|
+
input.context = input.context ?? JSON.stringify(context);
|
|
288
|
+
}
|
|
289
|
+
if (step.agent_slug === "publish-html-to-a-public-url") {
|
|
290
|
+
const previousOutputs = Array.isArray(input.previous_outputs) ? input.previous_outputs : [];
|
|
291
|
+
const writerOutput = previousOutputs[previousOutputs.length - 1];
|
|
292
|
+
const outputRecord = writerOutput && typeof writerOutput === "object"
|
|
293
|
+
? writerOutput
|
|
294
|
+
: null;
|
|
295
|
+
const output = outputRecord?.output && typeof outputRecord.output === "object"
|
|
296
|
+
? outputRecord.output
|
|
297
|
+
: outputRecord?.output;
|
|
298
|
+
input.html = input.html
|
|
299
|
+
?? (output && typeof output === "object" ? output.html : undefined)
|
|
300
|
+
?? (typeof output === "string" ? output : undefined)
|
|
301
|
+
?? JSON.stringify(output ?? writerOutput ?? {});
|
|
302
|
+
input.slug = input.slug ?? input.product_slug;
|
|
303
|
+
}
|
|
226
304
|
if (step.agent_slug === "place-search") {
|
|
227
305
|
input.location = input.location ?? input.zip;
|
|
228
306
|
}
|
|
@@ -499,7 +577,9 @@ export function registerPlaybookTools(server) {
|
|
|
499
577
|
budget: effectiveBudget,
|
|
500
578
|
limits: effectiveLimits,
|
|
501
579
|
});
|
|
502
|
-
|
|
580
|
+
if (quoted?.playbook) {
|
|
581
|
+
playbook = quoted.playbook;
|
|
582
|
+
}
|
|
503
583
|
}
|
|
504
584
|
catch {
|
|
505
585
|
// Quote analytics should not block the user-facing quote.
|
|
@@ -511,12 +591,13 @@ export function registerPlaybookTools(server) {
|
|
|
511
591
|
if (playbook.current_quote.blocking_issues.length > 0) {
|
|
512
592
|
return text(`Cannot run ${playbook.slug}; child-agent issues must be fixed first:\n${playbook.current_quote.blocking_issues.map((issue) => `- ${issue}`).join("\n")}`);
|
|
513
593
|
}
|
|
594
|
+
const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
|
|
514
595
|
const estimatedCost = playbook.current_quote.estimated_cost_usd;
|
|
515
596
|
const spendCheck = canSpend({ method: spendMethod, amountUsd: Math.min(effectiveBudget, estimatedCost) });
|
|
516
597
|
if (!spendCheck.ok)
|
|
517
598
|
return text(spendCheck.message);
|
|
518
599
|
if (estimatedCost > effectiveBudget) {
|
|
519
|
-
pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits:
|
|
600
|
+
pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: runLimits });
|
|
520
601
|
const cheaperAlternatives = await formatCheaperAlternatives(playbook.slug, effectiveBudget);
|
|
521
602
|
return text([
|
|
522
603
|
`${playbook.name} quote exceeds the budget.`,
|
|
@@ -528,11 +609,11 @@ export function registerPlaybookTools(server) {
|
|
|
528
609
|
...(cheaperAlternatives ? ["", cheaperAlternatives] : []),
|
|
529
610
|
"",
|
|
530
611
|
`Try a higher budget or reduced limits, then call:`,
|
|
531
|
-
formatRunConfirmationCommand(playbook.slug, method, Math.ceil(estimatedCost + 1),
|
|
612
|
+
formatRunConfirmationCommand(playbook.slug, method, Math.ceil(estimatedCost + 1), runLimits),
|
|
532
613
|
].join("\n"));
|
|
533
614
|
}
|
|
534
615
|
if ((requiresSpendConfirmation() || requiresPolicyConfirmation(spendMethod, estimatedCost)) && !confirmed) {
|
|
535
|
-
pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits:
|
|
616
|
+
pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: runLimits });
|
|
536
617
|
return text([
|
|
537
618
|
`Ready to run ${playbook.name}`,
|
|
538
619
|
"",
|
|
@@ -548,7 +629,7 @@ export function registerPlaybookTools(server) {
|
|
|
548
629
|
...formatQuoteNotes(playbook),
|
|
549
630
|
"",
|
|
550
631
|
"To proceed, call:",
|
|
551
|
-
formatRunConfirmationCommand(playbook.slug, method, effectiveBudget,
|
|
632
|
+
formatRunConfirmationCommand(playbook.slug, method, effectiveBudget, runLimits),
|
|
552
633
|
"",
|
|
553
634
|
"To cancel, do nothing.",
|
|
554
635
|
].join("\n"));
|
|
@@ -594,7 +675,7 @@ export function registerPlaybookTools(server) {
|
|
|
594
675
|
slug: playbook.slug,
|
|
595
676
|
input: processedInput,
|
|
596
677
|
budget: effectiveBudget,
|
|
597
|
-
limits:
|
|
678
|
+
limits: runLimits,
|
|
598
679
|
}),
|
|
599
680
|
completedStepIds: new Set(),
|
|
600
681
|
existingOutputs: [],
|
|
@@ -603,6 +684,14 @@ export function registerPlaybookTools(server) {
|
|
|
603
684
|
await apiPost(`/playbook-runs/${runState.run_id}/status`, { status: "running" });
|
|
604
685
|
let charged = runState.chargedUsd;
|
|
605
686
|
const childOutputs = [...runState.existingOutputs];
|
|
687
|
+
let linkSpendRequestId;
|
|
688
|
+
let linkNetworkId;
|
|
689
|
+
const linkApprovalContext = buildPlaybookLinkApprovalContext({
|
|
690
|
+
playbook,
|
|
691
|
+
budget: effectiveBudget,
|
|
692
|
+
stepCount: playbook.current_quote.step_count,
|
|
693
|
+
runId: runState.run_id,
|
|
694
|
+
});
|
|
606
695
|
for (const [iteration, step] of runState.steps.entries()) {
|
|
607
696
|
if (runState.completedStepIds.has(step.id)) {
|
|
608
697
|
continue;
|
|
@@ -650,24 +739,62 @@ export function registerPlaybookTools(server) {
|
|
|
650
739
|
let schemaError;
|
|
651
740
|
let usedPaidMethod = false;
|
|
652
741
|
const executeChild = async (childInput) => {
|
|
742
|
+
const body = { input: childInput, playbook_context: playbookContext };
|
|
653
743
|
try {
|
|
654
|
-
return await apiPost(`/agents/${step.agent_id}/run`,
|
|
744
|
+
return await apiPost(`/agents/${step.agent_id}/run`, body, { ensureConsumerPrincipal: true });
|
|
655
745
|
}
|
|
656
746
|
catch (err) {
|
|
657
747
|
const status = err?.status;
|
|
658
748
|
if (status !== 402)
|
|
659
749
|
throw err;
|
|
660
750
|
usedPaidMethod = true;
|
|
661
|
-
|
|
751
|
+
if ((method ?? spendMethod) === "link") {
|
|
752
|
+
const linkConfig = getLinkConfig();
|
|
753
|
+
if (!linkConfig) {
|
|
754
|
+
throw new Error('Payment method "link" is not configured. Run wallet_setup({ action: "add-link" }).');
|
|
755
|
+
}
|
|
756
|
+
const challenge = err?.headers?.get("www-authenticate");
|
|
757
|
+
if (!challenge && !linkNetworkId) {
|
|
758
|
+
throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
|
|
759
|
+
}
|
|
760
|
+
linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge);
|
|
761
|
+
linkSpendRequestId = linkSpendRequestId ?? await ensureApprovedLinkSpendRequest({
|
|
762
|
+
amount: cents(effectiveBudget),
|
|
763
|
+
currency: "usd",
|
|
764
|
+
context: linkApprovalContext,
|
|
765
|
+
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
|
766
|
+
networkId: linkNetworkId,
|
|
767
|
+
paymentMethodId: linkConfig.paymentMethodId,
|
|
768
|
+
});
|
|
769
|
+
return await apiPostWithApprovedLinkSpendRequest(`/agents/${step.agent_id}/run`, body, linkSpendRequestId);
|
|
770
|
+
}
|
|
771
|
+
return await apiPostWithPayment(`/agents/${step.agent_id}/run`, body, method);
|
|
662
772
|
}
|
|
663
773
|
};
|
|
664
774
|
try {
|
|
665
775
|
result = await executeChild(payload);
|
|
666
776
|
}
|
|
667
777
|
catch (err) {
|
|
778
|
+
if (err instanceof LinkApprovalRequiredError) {
|
|
779
|
+
await recordStep(runState.run_id, step.id, {
|
|
780
|
+
status: "pending",
|
|
781
|
+
agent_id: step.agent_id,
|
|
782
|
+
provider_id: step.provider_id,
|
|
783
|
+
consumption_mode: "not_charged",
|
|
784
|
+
error_code: null,
|
|
785
|
+
failure_message: null,
|
|
786
|
+
});
|
|
787
|
+
await updateRun(runState.run_id, {
|
|
788
|
+
status: "paused",
|
|
789
|
+
error_code: "LINK_APPROVAL_REQUIRED",
|
|
790
|
+
failure_message: "Approve the Link spend request, then resume the playbook run.",
|
|
791
|
+
});
|
|
792
|
+
const receipt = await apiGet(`/playbook-runs/${runState.run_id}`);
|
|
793
|
+
return text(`${formatReceipt(receipt)}\n\n${err.message}`);
|
|
794
|
+
}
|
|
668
795
|
const apiErr = err;
|
|
669
|
-
const validationFailed = apiErr?.status === 400
|
|
670
|
-
&& /missing required field|validation failed|invalid input|schema/i.test(apiErr.message ?? "");
|
|
796
|
+
const validationFailed = (apiErr?.status === 400 || apiErr?.status === 422)
|
|
797
|
+
&& /missing required field|validation failed|invalid input|schema|provide at least one/i.test(apiErr.message ?? "");
|
|
671
798
|
if (validationFailed) {
|
|
672
799
|
try {
|
|
673
800
|
const agent = await apiGet(`/agents/${step.agent_id}`);
|
|
@@ -729,13 +856,9 @@ export function registerPlaybookTools(server) {
|
|
|
729
856
|
return text(`${formatReceipt(receipt)}\n\nTried the live child schema but could not safely repair the input. Use get_agent({ agent_id: "${step.agent_id}" }) to inspect the child agent schema before retrying.`);
|
|
730
857
|
}
|
|
731
858
|
const jobId = result.job_id ?? "";
|
|
732
|
-
const resultAgentId = result.agent_id ?? step.agent_id;
|
|
733
859
|
const status = result.status;
|
|
734
860
|
let output = result.output;
|
|
735
861
|
let errorCode = result.error_code;
|
|
736
|
-
if (result.feedback_token && jobId) {
|
|
737
|
-
storeFeedbackToken(jobId, result.feedback_token, resultAgentId);
|
|
738
|
-
}
|
|
739
862
|
if (status === "processing" && jobId) {
|
|
740
863
|
const pollResult = await pollJobUntilDone(jobId, method);
|
|
741
864
|
output = pollResult.output;
|
|
@@ -834,10 +957,13 @@ export function registerPlaybookTools(server) {
|
|
|
834
957
|
});
|
|
835
958
|
const receipt = await apiGet(`/playbook-runs/${runState.run_id}`);
|
|
836
959
|
pendingPlaybookRuns.delete(playbook.slug);
|
|
960
|
+
if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
|
|
961
|
+
setPendingLinkSpendRequest(null);
|
|
962
|
+
}
|
|
837
963
|
const runBlocks = childOutputs.length > 0
|
|
838
964
|
? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)
|
|
839
965
|
: [];
|
|
840
|
-
return multiText(uploadSummary ? `${uploadSummary}\n\n${formatReceipt(receipt)}` : formatReceipt(receipt), ...runBlocks
|
|
966
|
+
return multiText(uploadSummary ? `${uploadSummary}\n\n${formatReceipt(receipt)}` : formatReceipt(receipt), ...runBlocks);
|
|
841
967
|
});
|
|
842
968
|
server.tool("favorite_playbook", "Save an Agent Wonderland Playbook for later. Favorites are associated with the configured consumer principal when available.", {
|
|
843
969
|
playbook_id: z.string().optional(),
|
|
@@ -849,28 +975,6 @@ export function registerPlaybookTools(server) {
|
|
|
849
975
|
const result = await apiPost(`/playbooks/${encodeURIComponent(id)}/favorite`, {}, { ensureConsumerPrincipal: true });
|
|
850
976
|
return text(`Saved ${result.playbook_slug} to favorites.`);
|
|
851
977
|
});
|
|
852
|
-
server.tool("rate_playbook", "Record feedback on an Agent Wonderland Playbook after a run. Provide a rating, useful flag, comment, or any combination.", {
|
|
853
|
-
playbook_id: z.string().optional(),
|
|
854
|
-
slug: z.string().optional(),
|
|
855
|
-
run_id: z.string().optional(),
|
|
856
|
-
rating: z.number().int().min(1).max(5).optional(),
|
|
857
|
-
useful: z.boolean().optional(),
|
|
858
|
-
comment: z.string().max(2000).optional(),
|
|
859
|
-
}, async ({ playbook_id, slug, run_id, rating, useful, comment }) => {
|
|
860
|
-
const id = slug ?? playbook_id;
|
|
861
|
-
if (!id)
|
|
862
|
-
return text("Provide slug or playbook_id.");
|
|
863
|
-
if (rating == null && useful == null && !comment?.trim()) {
|
|
864
|
-
return text("Provide rating, useful, or comment.");
|
|
865
|
-
}
|
|
866
|
-
const result = await apiPost(`/playbooks/${encodeURIComponent(id)}/feedback`, {
|
|
867
|
-
run_id,
|
|
868
|
-
rating,
|
|
869
|
-
useful,
|
|
870
|
-
comment,
|
|
871
|
-
}, { ensureConsumerPrincipal: true });
|
|
872
|
-
return text(`Recorded feedback for ${result.playbook_slug}.`);
|
|
873
|
-
});
|
|
874
978
|
server.tool("get_playbook_run", "Inspect current or historical Agent Wonderland Playbook run state, including completed steps, charged/refunded amount, partial output, and resume command when paused.", {
|
|
875
979
|
run_id: z.string(),
|
|
876
980
|
}, async ({ run_id }) => {
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
mockGetConsumerPrincipalForMethod,
|
|
11
11
|
mockGetBaseRebatePrincipal,
|
|
12
12
|
mockPaymentFetch,
|
|
13
|
+
mockPayMppWithApprovedLinkSpendRequest,
|
|
13
14
|
} = vi.hoisted(() => ({
|
|
14
15
|
mockGetApiUrl: vi.fn(),
|
|
15
16
|
mockGetApiKey: vi.fn(),
|
|
@@ -19,6 +20,7 @@ const {
|
|
|
19
20
|
mockGetConsumerPrincipalForMethod: vi.fn(),
|
|
20
21
|
mockGetBaseRebatePrincipal: vi.fn(),
|
|
21
22
|
mockPaymentFetch: vi.fn(),
|
|
23
|
+
mockPayMppWithApprovedLinkSpendRequest: vi.fn(),
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
26
|
vi.mock("../config.js", () => ({
|
|
@@ -39,6 +41,10 @@ vi.mock("../principal.js", () => ({
|
|
|
39
41
|
getBaseRebatePrincipal: (...args: unknown[]) => mockGetBaseRebatePrincipal(...args),
|
|
40
42
|
}));
|
|
41
43
|
|
|
44
|
+
vi.mock("../link-cli.js", () => ({
|
|
45
|
+
payMppWithApprovedLinkSpendRequest: (...args: unknown[]) => mockPayMppWithApprovedLinkSpendRequest(...args),
|
|
46
|
+
}));
|
|
47
|
+
|
|
42
48
|
describe("api-client headers", () => {
|
|
43
49
|
beforeEach(() => {
|
|
44
50
|
vi.clearAllMocks();
|
|
@@ -61,6 +67,7 @@ describe("api-client headers", () => {
|
|
|
61
67
|
status: 200,
|
|
62
68
|
headers: { "content-type": "application/json" },
|
|
63
69
|
}));
|
|
70
|
+
mockPayMppWithApprovedLinkSpendRequest.mockResolvedValue({ ok: true });
|
|
64
71
|
});
|
|
65
72
|
|
|
66
73
|
it("includes the Base rebate principal alongside the method-specific consumer principal", async () => {
|
|
@@ -130,4 +137,30 @@ describe("api-client headers", () => {
|
|
|
130
137
|
expect(calls[8]).toMatchObject({ url: "/playbook-runs/run-1", headers: { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "poll" } });
|
|
131
138
|
expect(calls[9]).toMatchObject({ url: "/playbook-runs/run-1/steps/ads", headers: { "X-AW-MCP-Tool": "run_playbook", "X-AW-MCP-Action": "execute" } });
|
|
132
139
|
});
|
|
140
|
+
|
|
141
|
+
it("uses Link MPP pay with run-agent headers for an approved spend request", async () => {
|
|
142
|
+
const { apiPostWithApprovedLinkSpendRequest } = await import("../api-client.js");
|
|
143
|
+
|
|
144
|
+
await apiPostWithApprovedLinkSpendRequest(
|
|
145
|
+
"/agents/agent-1/run",
|
|
146
|
+
{ input: { text: "hello" } },
|
|
147
|
+
"lsrq_test_123",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(mockPayMppWithApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
151
|
+
url: "https://api.agentwonderland.test/agents/agent-1/run",
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: { input: { text: "hello" } },
|
|
154
|
+
spendRequestId: "lsrq_test_123",
|
|
155
|
+
headers: expect.objectContaining({
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
Accept: "application/json",
|
|
158
|
+
"X-AW-Surface": "mcp",
|
|
159
|
+
"X-AW-MCP-Version": MCP_PACKAGE_VERSION,
|
|
160
|
+
"X-AW-MCP-Tool": "run_agent",
|
|
161
|
+
"X-AW-MCP-Action": "execute",
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
164
|
+
expect(mockEnsureConsumerPrincipalForMethod).toHaveBeenCalledWith("link");
|
|
165
|
+
});
|
|
133
166
|
});
|