@agentwonderland/mcp 0.1.54 → 0.1.55
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 +1 -1
- package/dist/tools/__tests__/playbooks.test.js +114 -34
- 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 +1 -1
- package/src/tools/__tests__/playbooks.test.ts +125 -46
- package/src/tools/playbooks.ts +177 -56
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
|
});
|
|
@@ -122,4 +122,114 @@ describe("Link CLI spend requests", () => {
|
|
|
122
122
|
expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
|
|
123
123
|
expect(state.pendingWrites).toEqual([null]);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
it("reuses an approved spend request id for playbook MPP payments", async () => {
|
|
127
|
+
const expiresAt = Math.floor(Date.now() / 1000) + 300;
|
|
128
|
+
state.pending = {
|
|
129
|
+
id: "lsrq_test_123",
|
|
130
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
131
|
+
amount: "500",
|
|
132
|
+
currency: "usd",
|
|
133
|
+
context: "Agent Wonderland playbook test context that is long enough for Link",
|
|
134
|
+
expiresAt,
|
|
135
|
+
networkId: "profile_test",
|
|
136
|
+
paymentMethodId: "csmrpd_test_123",
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
outputs.push({ id: "lsrq_test_123", status: "approved" });
|
|
140
|
+
|
|
141
|
+
const { ensureApprovedLinkSpendRequest } = await import("../link-cli.js");
|
|
142
|
+
|
|
143
|
+
await expect(ensureApprovedLinkSpendRequest({
|
|
144
|
+
amount: "500",
|
|
145
|
+
currency: "usd",
|
|
146
|
+
context: "Agent Wonderland playbook test context that is long enough for Link",
|
|
147
|
+
expiresAt,
|
|
148
|
+
networkId: "profile_test",
|
|
149
|
+
paymentMethodId: "csmrpd_test_123",
|
|
150
|
+
})).resolves.toBe("lsrq_test_123");
|
|
151
|
+
|
|
152
|
+
expect(execCalls).toHaveLength(1);
|
|
153
|
+
expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["spend-request", "retrieve", "lsrq_test_123"]));
|
|
154
|
+
expect(state.pendingWrites).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("pauses when the reusable Link spend request is still pending approval", async () => {
|
|
158
|
+
const expiresAt = Math.floor(Date.now() / 1000) + 300;
|
|
159
|
+
state.pending = {
|
|
160
|
+
id: "lsrq_test_123",
|
|
161
|
+
approvalUrl: "https://link.example/approve/lsrq_test_123",
|
|
162
|
+
amount: "500",
|
|
163
|
+
currency: "usd",
|
|
164
|
+
context: "Agent Wonderland playbook test context that is long enough for Link",
|
|
165
|
+
expiresAt,
|
|
166
|
+
networkId: "profile_test",
|
|
167
|
+
paymentMethodId: "csmrpd_test_123",
|
|
168
|
+
createdAt: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
outputs.push({ id: "lsrq_test_123", status: "pending_approval" });
|
|
171
|
+
|
|
172
|
+
const { ensureApprovedLinkSpendRequest, LinkApprovalRequiredError } = await import("../link-cli.js");
|
|
173
|
+
|
|
174
|
+
await expect(ensureApprovedLinkSpendRequest({
|
|
175
|
+
amount: "500",
|
|
176
|
+
currency: "usd",
|
|
177
|
+
context: "Agent Wonderland playbook test context that is long enough for Link",
|
|
178
|
+
expiresAt,
|
|
179
|
+
networkId: "profile_test",
|
|
180
|
+
paymentMethodId: "csmrpd_test_123",
|
|
181
|
+
})).rejects.toBeInstanceOf(LinkApprovalRequiredError);
|
|
182
|
+
|
|
183
|
+
expect(execCalls).toHaveLength(1);
|
|
184
|
+
expect(state.pendingWrites).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("runs Link MPP pay with an approved spend request and returns the response body", async () => {
|
|
188
|
+
outputs.push({
|
|
189
|
+
response: {
|
|
190
|
+
body: {
|
|
191
|
+
status: "success",
|
|
192
|
+
job_id: "job_123",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { payMppWithApprovedLinkSpendRequest } = await import("../link-cli.js");
|
|
198
|
+
|
|
199
|
+
await expect(payMppWithApprovedLinkSpendRequest({
|
|
200
|
+
url: "https://api.agentwonderland.test/agents/agent-1/run",
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
Accept: "application/json",
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
},
|
|
206
|
+
body: { input: { text: "hello" } },
|
|
207
|
+
spendRequestId: "lsrq_test_123",
|
|
208
|
+
})).resolves.toEqual({
|
|
209
|
+
status: "success",
|
|
210
|
+
job_id: "job_123",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(execCalls[0]?.args).toEqual(expect.arrayContaining([
|
|
214
|
+
"mpp",
|
|
215
|
+
"pay",
|
|
216
|
+
"https://api.agentwonderland.test/agents/agent-1/run",
|
|
217
|
+
"--spend-request-id",
|
|
218
|
+
"lsrq_test_123",
|
|
219
|
+
"--method",
|
|
220
|
+
"POST",
|
|
221
|
+
"--data",
|
|
222
|
+
JSON.stringify({ input: { text: "hello" } }),
|
|
223
|
+
]));
|
|
224
|
+
expect(execCalls[0]?.args).toContain("Accept: application/json");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("decodes the MPP network id from a payment challenge", async () => {
|
|
228
|
+
outputs.push({ accepts: [{ network_id: "profile_test" }] });
|
|
229
|
+
|
|
230
|
+
const { decodeMppNetworkId } = await import("../link-cli.js");
|
|
231
|
+
|
|
232
|
+
await expect(decodeMppNetworkId("Payment challenge")).resolves.toBe("profile_test");
|
|
233
|
+
expect(execCalls[0]?.args).toEqual(expect.arrayContaining(["mpp", "decode", "--challenge", "Payment challenge"]));
|
|
234
|
+
});
|
|
125
235
|
});
|
package/src/core/api-client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getConsumerPrincipalForMethod,
|
|
9
9
|
} from "./principal.js";
|
|
10
10
|
import { MCP_PACKAGE_VERSION } from "./version.js";
|
|
11
|
+
import { payMppWithApprovedLinkSpendRequest } from "./link-cli.js";
|
|
11
12
|
|
|
12
13
|
// ── Error class ────────────────────────────────────────────────────
|
|
13
14
|
|
|
@@ -16,6 +17,7 @@ export class ApiError extends Error {
|
|
|
16
17
|
public readonly status: number,
|
|
17
18
|
message: string,
|
|
18
19
|
public readonly body?: unknown,
|
|
20
|
+
public readonly headers?: Headers,
|
|
19
21
|
) {
|
|
20
22
|
super(message);
|
|
21
23
|
this.name = "ApiError";
|
|
@@ -121,7 +123,7 @@ async function handleResponse<T>(response: Response): Promise<T> {
|
|
|
121
123
|
: typeof body === "string"
|
|
122
124
|
? body
|
|
123
125
|
: `Request failed with status ${response.status}`;
|
|
124
|
-
throw new ApiError(response.status, message, body);
|
|
126
|
+
throw new ApiError(response.status, message, body, response.headers);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
return body as T;
|
|
@@ -205,6 +207,28 @@ export async function apiPostWithPayment<T>(
|
|
|
205
207
|
return attachResponseMetadata(result, response);
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
export async function apiPostWithApprovedLinkSpendRequest<T>(
|
|
211
|
+
path: string,
|
|
212
|
+
body: unknown,
|
|
213
|
+
spendRequestId: string,
|
|
214
|
+
options?: RequestOptions,
|
|
215
|
+
): Promise<T> {
|
|
216
|
+
const url = `${getApiUrl()}${path}`;
|
|
217
|
+
const headers = await buildHeaders(path, "POST", {
|
|
218
|
+
ensureConsumerPrincipal: true,
|
|
219
|
+
principalMethod: "link",
|
|
220
|
+
...options,
|
|
221
|
+
});
|
|
222
|
+
const result = await payMppWithApprovedLinkSpendRequest({
|
|
223
|
+
url,
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers,
|
|
226
|
+
body,
|
|
227
|
+
spendRequestId,
|
|
228
|
+
});
|
|
229
|
+
return result as T;
|
|
230
|
+
}
|
|
231
|
+
|
|
208
232
|
export async function apiPut<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {
|
|
209
233
|
const url = `${getApiUrl()}${path}`;
|
|
210
234
|
const response = await fetch(url, {
|