@agentwonderland/mcp 0.1.56 → 0.1.57

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.
@@ -1 +1 @@
1
- export declare const MCP_PACKAGE_VERSION = "0.1.56";
1
+ export declare const MCP_PACKAGE_VERSION = "0.1.57";
@@ -1 +1 @@
1
- export const MCP_PACKAGE_VERSION = "0.1.56";
1
+ export const MCP_PACKAGE_VERSION = "0.1.57";
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { registerRebateTools } from "./tools/rebates.js";
16
16
  import { registerUploadTools } from "./tools/upload.js";
17
17
  import { registerProbeTools } from "./tools/probe.js";
18
18
  import { registerProviderTools } from "./tools/providers.js";
19
+ import { registerPlaybookTools } from "./tools/playbooks.js";
19
20
  // ── Resources ────────────────────────────────────────────────────
20
21
  import { registerAgentResources } from "./resources/agents.js";
21
22
  import { registerWalletResources } from "./resources/wallet.js";
@@ -41,8 +42,10 @@ export async function startMcpServer() {
41
42
  "WORKFLOW:",
42
43
  "1. solve(intent, input, budget) — one call: find best agent + pay + run. Use when the task is clear.",
43
44
  " OR: search_agents() → get_agent() to inspect schema → run_agent() with required fields.",
45
+ " For multi-agent workflows, use search_playbooks() → get_playbook() → run_playbook().",
44
46
  "2. If the agent returns status 'processing', poll with get_job(). Async runs resolve automatically.",
45
47
  "3. After a successful agent run, rate_agent() and optionally tip_agent() if the result was useful.",
48
+ " Playbooks return one run-level receipt; do not ask users to rate or tip individual child-agent steps.",
46
49
  "4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
47
50
  "",
48
51
  "PAYMENT:",
@@ -85,6 +88,7 @@ export async function startMcpServer() {
85
88
  registerUploadTools(server);
86
89
  registerProbeTools(server);
87
90
  registerProviderTools(server);
91
+ registerPlaybookTools(server);
88
92
  // Register resources
89
93
  registerAgentResources(server);
90
94
  registerWalletResources(server);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "../playbook-adapters.js";
3
+ describe("playbook step adapters", () => {
4
+ it("maps ipo-brief child steps to their live input schemas", () => {
5
+ expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
6
+ id: "s1",
7
+ agent_slug: "ipo-s-1-analysis",
8
+ })).toMatchObject({ status: "ready", input: { ticker: "CRCL" } });
9
+ expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
10
+ id: "insiders",
11
+ agent_slug: "insider-trading-tracker",
12
+ })).toMatchObject({ status: "ready", input: { ticker: "CRCL", days_back: 365 } });
13
+ expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL", peer_tickers: ["COIN", "HOOD"] }, {
14
+ id: "comps",
15
+ agent_slug: "stock-comparison",
16
+ })).toMatchObject({ status: "ready", input: { tickers: ["CRCL", "COIN", "HOOD"] } });
17
+ });
18
+ it("maps launch-landing council, writer, and publisher steps explicitly", () => {
19
+ const council = buildExplicitPlaybookStepInput("launch-landing", {
20
+ brief: "Agent marketplace for builders",
21
+ product_slug: "agent-wonderland",
22
+ }, {
23
+ id: "council",
24
+ agent_slug: "marketing-copy-council",
25
+ });
26
+ expect(council).toMatchObject({ status: "ready", input: { brief: "Agent marketplace for builders" } });
27
+ const writer = buildExplicitPlaybookStepInput("launch-landing", {
28
+ brief: "Agent marketplace for builders",
29
+ product_slug: "agent-wonderland",
30
+ previous_outputs: [{ step: "council", output: { headline: "Hire agents on demand" } }],
31
+ }, {
32
+ id: "structure",
33
+ agent_slug: "write-landing-page-copy",
34
+ });
35
+ expect(writer).toMatchObject({
36
+ status: "ready",
37
+ input: {
38
+ product: "agent-wonderland",
39
+ audience: "builders and operators",
40
+ },
41
+ });
42
+ expect(writer.input.context).toContain("Hire agents on demand");
43
+ const publisher = buildExplicitPlaybookStepInput("launch-landing", {
44
+ brief: "Agent marketplace for builders",
45
+ product_slug: "agent-wonderland",
46
+ previous_outputs: [{ step: "structure", output: { html: "<html><body>Ship</body></html>" } }],
47
+ }, {
48
+ id: "publish",
49
+ agent_slug: "publish-html-to-a-public-url",
50
+ });
51
+ expect(publisher).toMatchObject({
52
+ status: "ready",
53
+ input: {
54
+ html: "<html><body>Ship</body></html>",
55
+ slug: "agent-wonderland",
56
+ },
57
+ });
58
+ });
59
+ it("blocks known broken fanout playbooks before paid child execution", () => {
60
+ expect(playbookRunBlocker("icp-hunter")).toContain("prospect-search schema");
61
+ expect(playbookRunBlocker("no-web-leads")).toContain("no-web/place/phone schemas");
62
+ expect(buildExplicitPlaybookStepInput("icp-hunter", { icp: "VP Eng" }, {
63
+ id: "prospects",
64
+ agent_slug: "find-prospects",
65
+ })).toMatchObject({ status: "blocked", code: "PLAYBOOK_NOT_CERTIFIED" });
66
+ });
67
+ });
@@ -533,7 +533,7 @@ describe("playbook MCP tools", () => {
533
533
  expect(text).not.toContain("favorite_playbook({ slug: \"competitor-ads\" })");
534
534
  expect(text).not.toContain("rate_playbook");
535
535
  });
536
- it("uses one approved Link spend request across paid playbook child steps", async () => {
536
+ it("uses a separate approved Link spend request for the paid child step", async () => {
537
537
  mockRequiresSpendConfirmation.mockReturnValue(false);
538
538
  mockGetConfiguredMethods.mockReturnValue(["link"]);
539
539
  mockApiGet
@@ -599,8 +599,9 @@ describe("playbook MCP tools", () => {
599
599
  const text = flattenToolText(result);
600
600
  expect(mockDecodeMppNetworkId).toHaveBeenCalledWith("Payment challenge");
601
601
  expect(mockEnsureApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
602
- amount: "500",
602
+ amount: "125",
603
603
  currency: "usd",
604
+ context: expect.stringContaining("step 1"),
604
605
  networkId: "profile_test",
605
606
  paymentMethodId: "csmrpd_test_123",
606
607
  }));
@@ -12,3 +12,4 @@ 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";
@@ -12,3 +12,4 @@ 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";
@@ -0,0 +1,19 @@
1
+ type StepLike = {
2
+ id: string;
3
+ agent_slug: string | null;
4
+ default_input?: Record<string, unknown>;
5
+ quantity?: number;
6
+ iteration?: number;
7
+ };
8
+ export type PlaybookStepInputResult = {
9
+ status: "ready";
10
+ input: Record<string, unknown>;
11
+ certification: "explicit";
12
+ } | {
13
+ status: "blocked";
14
+ code: string;
15
+ message: string;
16
+ };
17
+ export declare function playbookRunBlocker(slug: string): string | null;
18
+ export declare function buildExplicitPlaybookStepInput(playbookSlug: string, baseInput: Record<string, unknown>, step: StepLike): PlaybookStepInputResult | null;
19
+ export {};
@@ -0,0 +1,164 @@
1
+ const BLOCKED_PLAYBOOKS = {
2
+ "icp-hunter": "Blocked until the friendly ICP input is mapped to the live prospect-search schema and the prospect agent is verified to honor those filters.",
3
+ "no-web-leads": "Blocked until zip/category inputs are mapped to the live no-web/place/phone schemas and the no-web agent is verified to honor the requested local filters.",
4
+ };
5
+ function asString(value) {
6
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
7
+ }
8
+ function outputRecord(value) {
9
+ return value && typeof value === "object" && !Array.isArray(value)
10
+ ? value
11
+ : undefined;
12
+ }
13
+ function previousOutputs(input) {
14
+ return Array.isArray(input.previous_outputs)
15
+ ? input.previous_outputs.map(outputRecord).filter((item) => Boolean(item))
16
+ : [];
17
+ }
18
+ function stepOutput(entry) {
19
+ return outputRecord(entry)?.output ?? entry;
20
+ }
21
+ function extractHtml(value) {
22
+ if (typeof value === "string" && /<html|<!doctype|<body|<section|<main/i.test(value))
23
+ return value;
24
+ const record = outputRecord(value);
25
+ if (!record)
26
+ return undefined;
27
+ const direct = record.html;
28
+ if (typeof direct === "string" && direct.trim())
29
+ return direct;
30
+ const output = record.output;
31
+ if (typeof output === "string" && /<html|<!doctype|<body|<section|<main/i.test(output))
32
+ return output;
33
+ const nested = outputRecord(output);
34
+ return typeof nested?.html === "string" && nested.html.trim() ? nested.html : undefined;
35
+ }
36
+ function launchLandingInput(baseInput, step) {
37
+ const brief = asString(baseInput.brief) ?? asString(baseInput.context) ?? asString(baseInput.product);
38
+ if (!brief) {
39
+ return {
40
+ status: "blocked",
41
+ code: "PLAYBOOK_INPUT_MISSING_BRIEF",
42
+ message: "launch-landing requires a brief before any child agent is called.",
43
+ };
44
+ }
45
+ if (step.agent_slug === "marketing-copy-council") {
46
+ return {
47
+ status: "ready",
48
+ certification: "explicit",
49
+ input: {
50
+ brief,
51
+ context: {
52
+ product_slug: asString(baseInput.product_slug),
53
+ },
54
+ num_outputs: 1,
55
+ },
56
+ };
57
+ }
58
+ const outputs = previousOutputs(baseInput);
59
+ if (step.agent_slug === "write-landing-page-copy") {
60
+ const council = outputs.find((item) => item.step === "council" || item.step === "marketing-copy-council") ?? outputs.at(-1);
61
+ const product = asString(baseInput.product)
62
+ ?? asString(baseInput.product_name)
63
+ ?? asString(baseInput.product_slug)
64
+ ?? "Landing page";
65
+ return {
66
+ status: "ready",
67
+ certification: "explicit",
68
+ input: {
69
+ product,
70
+ audience: asString(baseInput.audience) ?? "builders and operators",
71
+ tone: asString(baseInput.tone) ?? "confident and practical",
72
+ context: JSON.stringify({
73
+ brief,
74
+ product_slug: asString(baseInput.product_slug),
75
+ council_output: stepOutput(council),
76
+ }),
77
+ },
78
+ };
79
+ }
80
+ if (step.agent_slug === "publish-html-to-a-public-url") {
81
+ const writer = outputs.find((item) => item.step === "structure" || item.step === "write-landing-page-copy") ?? outputs.at(-1);
82
+ const html = extractHtml(stepOutput(writer)) ?? extractHtml(writer);
83
+ if (!html) {
84
+ return {
85
+ status: "blocked",
86
+ code: "PLAYBOOK_INPUT_MISSING_HTML",
87
+ message: "launch-landing publish step requires generated HTML from the writer step.",
88
+ };
89
+ }
90
+ return {
91
+ status: "ready",
92
+ certification: "explicit",
93
+ input: {
94
+ html,
95
+ slug: asString(baseInput.product_slug) ?? asString(baseInput.slug),
96
+ title: asString(baseInput.title) ?? asString(baseInput.product) ?? asString(baseInput.product_slug),
97
+ },
98
+ };
99
+ }
100
+ return {
101
+ status: "blocked",
102
+ code: "PLAYBOOK_STEP_NOT_CERTIFIED",
103
+ message: `launch-landing step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
104
+ };
105
+ }
106
+ function ipoBriefInput(baseInput, step) {
107
+ const ticker = asString(baseInput.ticker) ?? asString(baseInput.symbol);
108
+ if (!ticker) {
109
+ return {
110
+ status: "blocked",
111
+ code: "PLAYBOOK_INPUT_MISSING_TICKER",
112
+ message: "ipo-brief requires ticker before any child agent is called.",
113
+ };
114
+ }
115
+ if (step.agent_slug === "ipo-s-1-analysis") {
116
+ return {
117
+ status: "ready",
118
+ certification: "explicit",
119
+ input: { ticker },
120
+ };
121
+ }
122
+ if (step.agent_slug === "insider-trading-tracker") {
123
+ return {
124
+ status: "ready",
125
+ certification: "explicit",
126
+ input: {
127
+ ticker,
128
+ days_back: Number(baseInput.days_back ?? 365),
129
+ },
130
+ };
131
+ }
132
+ if (step.agent_slug === "stock-comparison") {
133
+ const peers = Array.isArray(baseInput.peer_tickers) ? baseInput.peer_tickers : [];
134
+ const tickers = [ticker, ...peers].filter((item) => typeof item === "string" && Boolean(item.trim())).slice(0, 5);
135
+ return {
136
+ status: "ready",
137
+ certification: "explicit",
138
+ input: { tickers },
139
+ };
140
+ }
141
+ return {
142
+ status: "blocked",
143
+ code: "PLAYBOOK_STEP_NOT_CERTIFIED",
144
+ message: `ipo-brief step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
145
+ };
146
+ }
147
+ export function playbookRunBlocker(slug) {
148
+ return BLOCKED_PLAYBOOKS[slug] ?? null;
149
+ }
150
+ export function buildExplicitPlaybookStepInput(playbookSlug, baseInput, step) {
151
+ const blocker = playbookRunBlocker(playbookSlug);
152
+ if (blocker) {
153
+ return {
154
+ status: "blocked",
155
+ code: "PLAYBOOK_NOT_CERTIFIED",
156
+ message: blocker,
157
+ };
158
+ }
159
+ if (playbookSlug === "launch-landing")
160
+ return launchLandingInput(baseInput, step);
161
+ if (playbookSlug === "ipo-brief")
162
+ return ipoBriefInput(baseInput, step);
163
+ return null;
164
+ }
@@ -7,6 +7,7 @@ import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend
7
7
  import { formatRunResult } from "../core/formatters.js";
8
8
  import { decodeMppNetworkId, ensureApprovedLinkSpendRequest, LinkApprovalRequiredError, } from "../core/link-cli.js";
9
9
  import { formatPaymentLabel, resolveConfirmationMethod, } from "./_payment-confirmation.js";
10
+ import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "./playbook-adapters.js";
10
11
  const POLL_INTERVAL_MS = 3000;
11
12
  const POLL_MAX_MS = 300000;
12
13
  const pendingPlaybookRuns = new Map();
@@ -33,9 +34,9 @@ function fanoutControls(playbook) {
33
34
  }
34
35
  function buildPlaybookLinkApprovalContext(params) {
35
36
  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.",
37
+ `Approve ${money(params.step.quoted_cost_usd)} for step ${params.step.index + 1} of the Agent Wonderland "${params.playbook.name}" playbook.`,
38
+ `Run ${params.runId}, step ${params.step.id}: ${params.step.agent_name}.`,
39
+ "Each child agent is approved separately, and failed child runs keep the existing Agent Wonderland refund behavior.",
39
40
  ].join(" ");
40
41
  }
41
42
  async function collectWalletAddresses(paymentMethod) {
@@ -217,7 +218,15 @@ async function formatCheaperAlternatives(currentSlug, budget) {
217
218
  return "";
218
219
  }
219
220
  }
220
- function stepInput(baseInput, step, iteration) {
221
+ function stepInput(playbookSlug, baseInput, step, iteration) {
222
+ const explicit = buildExplicitPlaybookStepInput(playbookSlug, baseInput, step);
223
+ if (explicit?.status === "ready")
224
+ return explicit.input;
225
+ if (explicit?.status === "blocked") {
226
+ const error = new Error(explicit.message);
227
+ error.code = explicit.code;
228
+ throw error;
229
+ }
221
230
  const input = { ...(step.default_input ?? {}), ...baseInput };
222
231
  const itemIndex = Math.max(0, (step.iteration ?? 1) - 1);
223
232
  if (Array.isArray(input.leads) && input.leads[itemIndex] && typeof input.leads[itemIndex] === "object") {
@@ -591,6 +600,10 @@ export function registerPlaybookTools(server) {
591
600
  if (playbook.current_quote.blocking_issues.length > 0) {
592
601
  return text(`Cannot run ${playbook.slug}; child-agent issues must be fixed first:\n${playbook.current_quote.blocking_issues.map((issue) => `- ${issue}`).join("\n")}`);
593
602
  }
603
+ const blocker = playbookRunBlocker(playbook.slug);
604
+ if (blocker) {
605
+ return text(`Playbook ${playbook.slug} is temporarily paused for certification.\n\n${blocker}`);
606
+ }
594
607
  const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
595
608
  const estimatedCost = playbook.current_quote.estimated_cost_usd;
596
609
  const spendCheck = canSpend({ method: spendMethod, amountUsd: Math.min(effectiveBudget, estimatedCost) });
@@ -684,14 +697,7 @@ export function registerPlaybookTools(server) {
684
697
  await apiPost(`/playbook-runs/${runState.run_id}/status`, { status: "running" });
685
698
  let charged = runState.chargedUsd;
686
699
  const childOutputs = [...runState.existingOutputs];
687
- let linkSpendRequestId;
688
700
  let linkNetworkId;
689
- const linkApprovalContext = buildPlaybookLinkApprovalContext({
690
- playbook,
691
- budget: effectiveBudget,
692
- stepCount: playbook.current_quote.step_count,
693
- runId: runState.run_id,
694
- });
695
701
  for (const [iteration, step] of runState.steps.entries()) {
696
702
  if (runState.completedStepIds.has(step.id)) {
697
703
  continue;
@@ -727,7 +733,28 @@ export function registerPlaybookTools(server) {
727
733
  provider_id: step.provider_id,
728
734
  quoted_cost_usd: step.quoted_cost_usd,
729
735
  });
730
- const payload = stepInput({ ...processedInput, previous_outputs: childOutputs }, step, iteration);
736
+ let payload;
737
+ try {
738
+ payload = stepInput(playbook.slug, { ...processedInput, previous_outputs: childOutputs }, step, iteration);
739
+ }
740
+ catch (err) {
741
+ const blocked = err;
742
+ await recordStep(runState.run_id, step.id, {
743
+ status: "skipped",
744
+ agent_id: step.agent_id,
745
+ provider_id: step.provider_id,
746
+ consumption_mode: "not_charged",
747
+ error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
748
+ failure_message: blocked.message,
749
+ });
750
+ await updateRun(runState.run_id, {
751
+ status: "paused",
752
+ error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
753
+ failure_message: blocked.message,
754
+ });
755
+ const receipt = await apiGet(`/playbook-runs/${runState.run_id}`);
756
+ return text(`${formatReceipt(receipt)}\n\n${blocked.message}`);
757
+ }
731
758
  const playbookContext = {
732
759
  playbook_run_id: runState.run_id,
733
760
  playbook_id: playbook.id,
@@ -738,6 +765,7 @@ export function registerPlaybookTools(server) {
738
765
  let result;
739
766
  let schemaError;
740
767
  let usedPaidMethod = false;
768
+ let stepLinkSpendRequestId;
741
769
  const executeChild = async (childInput) => {
742
770
  const body = { input: childInput, playbook_context: playbookContext };
743
771
  try {
@@ -758,15 +786,19 @@ export function registerPlaybookTools(server) {
758
786
  throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
759
787
  }
760
788
  linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge);
761
- linkSpendRequestId = linkSpendRequestId ?? await ensureApprovedLinkSpendRequest({
762
- amount: cents(effectiveBudget),
789
+ stepLinkSpendRequestId = await ensureApprovedLinkSpendRequest({
790
+ amount: cents(step.quoted_cost_usd),
763
791
  currency: "usd",
764
- context: linkApprovalContext,
792
+ context: buildPlaybookLinkApprovalContext({
793
+ playbook,
794
+ runId: runState.run_id,
795
+ step,
796
+ }),
765
797
  expiresAt: Math.floor(Date.now() / 1000) + 3600,
766
798
  networkId: linkNetworkId,
767
799
  paymentMethodId: linkConfig.paymentMethodId,
768
800
  });
769
- return await apiPostWithApprovedLinkSpendRequest(`/agents/${step.agent_id}/run`, body, linkSpendRequestId);
801
+ return await apiPostWithApprovedLinkSpendRequest(`/agents/${step.agent_id}/run`, body, stepLinkSpendRequestId);
770
802
  }
771
803
  return await apiPostWithPayment(`/agents/${step.agent_id}/run`, body, method);
772
804
  }
@@ -818,6 +850,9 @@ export function registerPlaybookTools(server) {
818
850
  }
819
851
  else {
820
852
  const errorCode = apiErr?.status === 402 ? "PAYMENT_REJECTED" : "CHILD_AGENT_FAILED";
853
+ if ((method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
854
+ setPendingLinkSpendRequest(null);
855
+ }
821
856
  await recordStep(runState.run_id, step.id, {
822
857
  status: apiErr?.status === 402 ? "skipped" : "failed",
823
858
  agent_id: step.agent_id,
@@ -838,6 +873,9 @@ export function registerPlaybookTools(server) {
838
873
  return text(`${formatReceipt(receipt)}\n\n${help}`);
839
874
  }
840
875
  }
876
+ if (result && (method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
877
+ setPendingLinkSpendRequest(null);
878
+ }
841
879
  if (!result) {
842
880
  await recordStep(runState.run_id, step.id, {
843
881
  status: "skipped",
@@ -957,9 +995,6 @@ export function registerPlaybookTools(server) {
957
995
  });
958
996
  const receipt = await apiGet(`/playbook-runs/${runState.run_id}`);
959
997
  pendingPlaybookRuns.delete(playbook.slug);
960
- if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
961
- setPendingLinkSpendRequest(null);
962
- }
963
998
  const runBlocks = childOutputs.length > 0
964
999
  ? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)
965
1000
  : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -1 +1 @@
1
- export const MCP_PACKAGE_VERSION = "0.1.56";
1
+ export const MCP_PACKAGE_VERSION = "0.1.57";
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,8 +49,10 @@ 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
54
  "3. After a successful agent run, rate_agent() and optionally tip_agent() if the result was useful.",
55
+ " Playbooks return one run-level receipt; do not ask users to rate or tip individual child-agent steps.",
53
56
  "4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
54
57
  "",
55
58
  "PAYMENT:",
@@ -94,6 +97,7 @@ export async function startMcpServer(): Promise<void> {
94
97
  registerUploadTools(server);
95
98
  registerProbeTools(server);
96
99
  registerProviderTools(server);
100
+ registerPlaybookTools(server);
97
101
 
98
102
  // Register resources
99
103
  registerAgentResources(server);
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "../playbook-adapters.js";
3
+
4
+ describe("playbook step adapters", () => {
5
+ it("maps ipo-brief child steps to their live input schemas", () => {
6
+ expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
7
+ id: "s1",
8
+ agent_slug: "ipo-s-1-analysis",
9
+ })).toMatchObject({ status: "ready", input: { ticker: "CRCL" } });
10
+
11
+ expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL" }, {
12
+ id: "insiders",
13
+ agent_slug: "insider-trading-tracker",
14
+ })).toMatchObject({ status: "ready", input: { ticker: "CRCL", days_back: 365 } });
15
+
16
+ expect(buildExplicitPlaybookStepInput("ipo-brief", { ticker: "CRCL", peer_tickers: ["COIN", "HOOD"] }, {
17
+ id: "comps",
18
+ agent_slug: "stock-comparison",
19
+ })).toMatchObject({ status: "ready", input: { tickers: ["CRCL", "COIN", "HOOD"] } });
20
+ });
21
+
22
+ it("maps launch-landing council, writer, and publisher steps explicitly", () => {
23
+ const council = buildExplicitPlaybookStepInput("launch-landing", {
24
+ brief: "Agent marketplace for builders",
25
+ product_slug: "agent-wonderland",
26
+ }, {
27
+ id: "council",
28
+ agent_slug: "marketing-copy-council",
29
+ });
30
+ expect(council).toMatchObject({ status: "ready", input: { brief: "Agent marketplace for builders" } });
31
+
32
+ const writer = buildExplicitPlaybookStepInput("launch-landing", {
33
+ brief: "Agent marketplace for builders",
34
+ product_slug: "agent-wonderland",
35
+ previous_outputs: [{ step: "council", output: { headline: "Hire agents on demand" } }],
36
+ }, {
37
+ id: "structure",
38
+ agent_slug: "write-landing-page-copy",
39
+ });
40
+ expect(writer).toMatchObject({
41
+ status: "ready",
42
+ input: {
43
+ product: "agent-wonderland",
44
+ audience: "builders and operators",
45
+ },
46
+ });
47
+ expect((writer as { input: Record<string, unknown> }).input.context).toContain("Hire agents on demand");
48
+
49
+ const publisher = buildExplicitPlaybookStepInput("launch-landing", {
50
+ brief: "Agent marketplace for builders",
51
+ product_slug: "agent-wonderland",
52
+ previous_outputs: [{ step: "structure", output: { html: "<html><body>Ship</body></html>" } }],
53
+ }, {
54
+ id: "publish",
55
+ agent_slug: "publish-html-to-a-public-url",
56
+ });
57
+ expect(publisher).toMatchObject({
58
+ status: "ready",
59
+ input: {
60
+ html: "<html><body>Ship</body></html>",
61
+ slug: "agent-wonderland",
62
+ },
63
+ });
64
+ });
65
+
66
+ it("blocks known broken fanout playbooks before paid child execution", () => {
67
+ expect(playbookRunBlocker("icp-hunter")).toContain("prospect-search schema");
68
+ expect(playbookRunBlocker("no-web-leads")).toContain("no-web/place/phone schemas");
69
+ expect(buildExplicitPlaybookStepInput("icp-hunter", { icp: "VP Eng" }, {
70
+ id: "prospects",
71
+ agent_slug: "find-prospects",
72
+ })).toMatchObject({ status: "blocked", code: "PLAYBOOK_NOT_CERTIFIED" });
73
+ });
74
+ });
@@ -621,7 +621,7 @@ describe("playbook MCP tools", () => {
621
621
  expect(text).not.toContain("rate_playbook");
622
622
  });
623
623
 
624
- it("uses one approved Link spend request across paid playbook child steps", async () => {
624
+ it("uses a separate approved Link spend request for the paid child step", async () => {
625
625
  mockRequiresSpendConfirmation.mockReturnValue(false);
626
626
  mockGetConfiguredMethods.mockReturnValue(["link"]);
627
627
  mockApiGet
@@ -690,8 +690,9 @@ describe("playbook MCP tools", () => {
690
690
 
691
691
  expect(mockDecodeMppNetworkId).toHaveBeenCalledWith("Payment challenge");
692
692
  expect(mockEnsureApprovedLinkSpendRequest).toHaveBeenCalledWith(expect.objectContaining({
693
- amount: "500",
693
+ amount: "125",
694
694
  currency: "usd",
695
+ context: expect.stringContaining("step 1"),
695
696
  networkId: "profile_test",
696
697
  paymentMethodId: "csmrpd_test_123",
697
698
  }));
@@ -12,3 +12,4 @@ 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";
@@ -0,0 +1,199 @@
1
+ type StepLike = {
2
+ id: string;
3
+ agent_slug: string | null;
4
+ default_input?: Record<string, unknown>;
5
+ quantity?: number;
6
+ iteration?: number;
7
+ };
8
+
9
+ export type PlaybookStepInputResult =
10
+ | {
11
+ status: "ready";
12
+ input: Record<string, unknown>;
13
+ certification: "explicit";
14
+ }
15
+ | {
16
+ status: "blocked";
17
+ code: string;
18
+ message: string;
19
+ };
20
+
21
+ const BLOCKED_PLAYBOOKS: Record<string, string> = {
22
+ "icp-hunter": "Blocked until the friendly ICP input is mapped to the live prospect-search schema and the prospect agent is verified to honor those filters.",
23
+ "no-web-leads": "Blocked until zip/category inputs are mapped to the live no-web/place/phone schemas and the no-web agent is verified to honor the requested local filters.",
24
+ };
25
+
26
+ function asString(value: unknown): string | undefined {
27
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
28
+ }
29
+
30
+ function outputRecord(value: unknown): Record<string, unknown> | undefined {
31
+ return value && typeof value === "object" && !Array.isArray(value)
32
+ ? value as Record<string, unknown>
33
+ : undefined;
34
+ }
35
+
36
+ function previousOutputs(input: Record<string, unknown>): Array<Record<string, unknown>> {
37
+ return Array.isArray(input.previous_outputs)
38
+ ? input.previous_outputs.map(outputRecord).filter((item): item is Record<string, unknown> => Boolean(item))
39
+ : [];
40
+ }
41
+
42
+ function stepOutput(entry: Record<string, unknown> | undefined): unknown {
43
+ return outputRecord(entry)?.output ?? entry;
44
+ }
45
+
46
+ function extractHtml(value: unknown): string | undefined {
47
+ if (typeof value === "string" && /<html|<!doctype|<body|<section|<main/i.test(value)) return value;
48
+ const record = outputRecord(value);
49
+ if (!record) return undefined;
50
+ const direct = record.html;
51
+ if (typeof direct === "string" && direct.trim()) return direct;
52
+ const output = record.output;
53
+ if (typeof output === "string" && /<html|<!doctype|<body|<section|<main/i.test(output)) return output;
54
+ const nested = outputRecord(output);
55
+ return typeof nested?.html === "string" && nested.html.trim() ? nested.html : undefined;
56
+ }
57
+
58
+ function launchLandingInput(baseInput: Record<string, unknown>, step: StepLike): PlaybookStepInputResult {
59
+ const brief = asString(baseInput.brief) ?? asString(baseInput.context) ?? asString(baseInput.product);
60
+ if (!brief) {
61
+ return {
62
+ status: "blocked",
63
+ code: "PLAYBOOK_INPUT_MISSING_BRIEF",
64
+ message: "launch-landing requires a brief before any child agent is called.",
65
+ };
66
+ }
67
+
68
+ if (step.agent_slug === "marketing-copy-council") {
69
+ return {
70
+ status: "ready",
71
+ certification: "explicit",
72
+ input: {
73
+ brief,
74
+ context: {
75
+ product_slug: asString(baseInput.product_slug),
76
+ },
77
+ num_outputs: 1,
78
+ },
79
+ };
80
+ }
81
+
82
+ const outputs = previousOutputs(baseInput);
83
+ if (step.agent_slug === "write-landing-page-copy") {
84
+ const council = outputs.find((item) => item.step === "council" || item.step === "marketing-copy-council") ?? outputs.at(-1);
85
+ const product = asString(baseInput.product)
86
+ ?? asString(baseInput.product_name)
87
+ ?? asString(baseInput.product_slug)
88
+ ?? "Landing page";
89
+
90
+ return {
91
+ status: "ready",
92
+ certification: "explicit",
93
+ input: {
94
+ product,
95
+ audience: asString(baseInput.audience) ?? "builders and operators",
96
+ tone: asString(baseInput.tone) ?? "confident and practical",
97
+ context: JSON.stringify({
98
+ brief,
99
+ product_slug: asString(baseInput.product_slug),
100
+ council_output: stepOutput(council),
101
+ }),
102
+ },
103
+ };
104
+ }
105
+
106
+ if (step.agent_slug === "publish-html-to-a-public-url") {
107
+ const writer = outputs.find((item) => item.step === "structure" || item.step === "write-landing-page-copy") ?? outputs.at(-1);
108
+ const html = extractHtml(stepOutput(writer)) ?? extractHtml(writer);
109
+ if (!html) {
110
+ return {
111
+ status: "blocked",
112
+ code: "PLAYBOOK_INPUT_MISSING_HTML",
113
+ message: "launch-landing publish step requires generated HTML from the writer step.",
114
+ };
115
+ }
116
+ return {
117
+ status: "ready",
118
+ certification: "explicit",
119
+ input: {
120
+ html,
121
+ slug: asString(baseInput.product_slug) ?? asString(baseInput.slug),
122
+ title: asString(baseInput.title) ?? asString(baseInput.product) ?? asString(baseInput.product_slug),
123
+ },
124
+ };
125
+ }
126
+
127
+ return {
128
+ status: "blocked",
129
+ code: "PLAYBOOK_STEP_NOT_CERTIFIED",
130
+ message: `launch-landing step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
131
+ };
132
+ }
133
+
134
+ function ipoBriefInput(baseInput: Record<string, unknown>, step: StepLike): PlaybookStepInputResult {
135
+ const ticker = asString(baseInput.ticker) ?? asString(baseInput.symbol);
136
+ if (!ticker) {
137
+ return {
138
+ status: "blocked",
139
+ code: "PLAYBOOK_INPUT_MISSING_TICKER",
140
+ message: "ipo-brief requires ticker before any child agent is called.",
141
+ };
142
+ }
143
+
144
+ if (step.agent_slug === "ipo-s-1-analysis") {
145
+ return {
146
+ status: "ready",
147
+ certification: "explicit",
148
+ input: { ticker },
149
+ };
150
+ }
151
+ if (step.agent_slug === "insider-trading-tracker") {
152
+ return {
153
+ status: "ready",
154
+ certification: "explicit",
155
+ input: {
156
+ ticker,
157
+ days_back: Number(baseInput.days_back ?? 365),
158
+ },
159
+ };
160
+ }
161
+ if (step.agent_slug === "stock-comparison") {
162
+ const peers = Array.isArray(baseInput.peer_tickers) ? baseInput.peer_tickers : [];
163
+ const tickers = [ticker, ...peers].filter((item): item is string => typeof item === "string" && Boolean(item.trim())).slice(0, 5);
164
+ return {
165
+ status: "ready",
166
+ certification: "explicit",
167
+ input: { tickers },
168
+ };
169
+ }
170
+
171
+ return {
172
+ status: "blocked",
173
+ code: "PLAYBOOK_STEP_NOT_CERTIFIED",
174
+ message: `ipo-brief step ${step.id} (${step.agent_slug ?? "unknown"}) has no explicit adapter.`,
175
+ };
176
+ }
177
+
178
+ export function playbookRunBlocker(slug: string): string | null {
179
+ return BLOCKED_PLAYBOOKS[slug] ?? null;
180
+ }
181
+
182
+ export function buildExplicitPlaybookStepInput(
183
+ playbookSlug: string,
184
+ baseInput: Record<string, unknown>,
185
+ step: StepLike,
186
+ ): PlaybookStepInputResult | null {
187
+ const blocker = playbookRunBlocker(playbookSlug);
188
+ if (blocker) {
189
+ return {
190
+ status: "blocked",
191
+ code: "PLAYBOOK_NOT_CERTIFIED",
192
+ message: blocker,
193
+ };
194
+ }
195
+
196
+ if (playbookSlug === "launch-landing") return launchLandingInput(baseInput, step);
197
+ if (playbookSlug === "ipo-brief") return ipoBriefInput(baseInput, step);
198
+ return null;
199
+ }
@@ -15,6 +15,7 @@ import {
15
15
  formatPaymentLabel,
16
16
  resolveConfirmationMethod,
17
17
  } from "./_payment-confirmation.js";
18
+ import { buildExplicitPlaybookStepInput, playbookRunBlocker } from "./playbook-adapters.js";
18
19
 
19
20
  const POLL_INTERVAL_MS = 3000;
20
21
  const POLL_MAX_MS = 300000;
@@ -163,14 +164,13 @@ function fanoutControls(playbook: PlaybookRecord): PlaybookFanoutControl[] {
163
164
 
164
165
  function buildPlaybookLinkApprovalContext(params: {
165
166
  playbook: PlaybookRecord;
166
- budget: number;
167
- stepCount: number;
168
167
  runId: string;
168
+ step: PlaybookStepQuote;
169
169
  }): string {
170
170
  return [
171
- `Approve up to ${money(params.budget)} for the Agent Wonderland "${params.playbook.name}" playbook.`,
172
- `This playbook run ${params.runId} has ${params.stepCount} child-agent steps and will stop before exceeding the approved budget.`,
173
- "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.",
171
+ `Approve ${money(params.step.quoted_cost_usd)} for step ${params.step.index + 1} of the Agent Wonderland "${params.playbook.name}" playbook.`,
172
+ `Run ${params.runId}, step ${params.step.id}: ${params.step.agent_name}.`,
173
+ "Each child agent is approved separately, and failed child runs keep the existing Agent Wonderland refund behavior.",
174
174
  ].join(" ");
175
175
  }
176
176
 
@@ -374,7 +374,15 @@ async function formatCheaperAlternatives(currentSlug: string, budget: number): P
374
374
  }
375
375
  }
376
376
 
377
- function stepInput(baseInput: Record<string, unknown>, step: PlaybookStepQuote, iteration: number): Record<string, unknown> {
377
+ function stepInput(playbookSlug: string, baseInput: Record<string, unknown>, step: PlaybookStepQuote, iteration: number): Record<string, unknown> {
378
+ const explicit = buildExplicitPlaybookStepInput(playbookSlug, baseInput, step);
379
+ if (explicit?.status === "ready") return explicit.input;
380
+ if (explicit?.status === "blocked") {
381
+ const error = new Error(explicit.message) as Error & { code?: string };
382
+ error.code = explicit.code;
383
+ throw error;
384
+ }
385
+
378
386
  const input: Record<string, unknown> = { ...(step.default_input ?? {}), ...baseInput };
379
387
  const itemIndex = Math.max(0, (step.iteration ?? 1) - 1);
380
388
 
@@ -778,6 +786,10 @@ export function registerPlaybookTools(server: McpServer): void {
778
786
  if (playbook.current_quote.blocking_issues.length > 0) {
779
787
  return text(`Cannot run ${playbook.slug}; child-agent issues must be fixed first:\n${playbook.current_quote.blocking_issues.map((issue) => `- ${issue}`).join("\n")}`);
780
788
  }
789
+ const blocker = playbookRunBlocker(playbook.slug);
790
+ if (blocker) {
791
+ return text(`Playbook ${playbook.slug} is temporarily paused for certification.\n\n${blocker}`);
792
+ }
781
793
 
782
794
  const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
783
795
  const estimatedCost = playbook.current_quote.estimated_cost_usd;
@@ -880,14 +892,7 @@ export function registerPlaybookTools(server: McpServer): void {
880
892
 
881
893
  let charged = runState.chargedUsd;
882
894
  const childOutputs: Array<{ step: string; job_id?: string; output?: unknown }> = [...runState.existingOutputs];
883
- let linkSpendRequestId: string | undefined;
884
895
  let linkNetworkId: string | undefined;
885
- const linkApprovalContext = buildPlaybookLinkApprovalContext({
886
- playbook,
887
- budget: effectiveBudget,
888
- stepCount: playbook.current_quote.step_count,
889
- runId: runState.run_id,
890
- });
891
896
 
892
897
  for (const [iteration, step] of runState.steps.entries()) {
893
898
  if (runState.completedStepIds.has(step.id)) {
@@ -927,7 +932,27 @@ export function registerPlaybookTools(server: McpServer): void {
927
932
  quoted_cost_usd: step.quoted_cost_usd,
928
933
  });
929
934
 
930
- const payload = stepInput({ ...processedInput, previous_outputs: childOutputs }, step, iteration);
935
+ let payload: Record<string, unknown>;
936
+ try {
937
+ payload = stepInput(playbook.slug, { ...processedInput, previous_outputs: childOutputs }, step, iteration);
938
+ } catch (err) {
939
+ const blocked = err as Error & { code?: string };
940
+ await recordStep(runState.run_id, step.id, {
941
+ status: "skipped",
942
+ agent_id: step.agent_id,
943
+ provider_id: step.provider_id,
944
+ consumption_mode: "not_charged",
945
+ error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
946
+ failure_message: blocked.message,
947
+ });
948
+ await updateRun(runState.run_id, {
949
+ status: "paused",
950
+ error_code: blocked.code ?? "PLAYBOOK_STEP_INPUT_BLOCKED",
951
+ failure_message: blocked.message,
952
+ });
953
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
954
+ return text(`${formatReceipt(receipt)}\n\n${blocked.message}`);
955
+ }
931
956
  const playbookContext = {
932
957
  playbook_run_id: runState.run_id,
933
958
  playbook_id: playbook.id,
@@ -938,6 +963,7 @@ export function registerPlaybookTools(server: McpServer): void {
938
963
  let result: Record<string, unknown> | undefined;
939
964
  let schemaError: { status?: number; message?: string } | undefined;
940
965
  let usedPaidMethod = false;
966
+ let stepLinkSpendRequestId: string | undefined;
941
967
  const executeChild = async (childInput: Record<string, unknown>) => {
942
968
  const body = { input: childInput, playbook_context: playbookContext };
943
969
  try {
@@ -960,10 +986,14 @@ export function registerPlaybookTools(server: McpServer): void {
960
986
  throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
961
987
  }
962
988
  linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge!);
963
- linkSpendRequestId = linkSpendRequestId ?? await ensureApprovedLinkSpendRequest({
964
- amount: cents(effectiveBudget),
989
+ stepLinkSpendRequestId = await ensureApprovedLinkSpendRequest({
990
+ amount: cents(step.quoted_cost_usd),
965
991
  currency: "usd",
966
- context: linkApprovalContext,
992
+ context: buildPlaybookLinkApprovalContext({
993
+ playbook,
994
+ runId: runState.run_id,
995
+ step,
996
+ }),
967
997
  expiresAt: Math.floor(Date.now() / 1000) + 3600,
968
998
  networkId: linkNetworkId,
969
999
  paymentMethodId: linkConfig.paymentMethodId,
@@ -971,7 +1001,7 @@ export function registerPlaybookTools(server: McpServer): void {
971
1001
  return await apiPostWithApprovedLinkSpendRequest<Record<string, unknown>>(
972
1002
  `/agents/${step.agent_id}/run`,
973
1003
  body,
974
- linkSpendRequestId,
1004
+ stepLinkSpendRequestId,
975
1005
  );
976
1006
  }
977
1007
  return await apiPostWithPayment<Record<string, unknown>>(
@@ -1023,6 +1053,9 @@ export function registerPlaybookTools(server: McpServer): void {
1023
1053
  }
1024
1054
  } else {
1025
1055
  const errorCode = apiErr?.status === 402 ? "PAYMENT_REJECTED" : "CHILD_AGENT_FAILED";
1056
+ if ((method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
1057
+ setPendingLinkSpendRequest(null);
1058
+ }
1026
1059
  await recordStep(runState.run_id, step.id, {
1027
1060
  status: apiErr?.status === 402 ? "skipped" : "failed",
1028
1061
  agent_id: step.agent_id,
@@ -1044,6 +1077,10 @@ export function registerPlaybookTools(server: McpServer): void {
1044
1077
  }
1045
1078
  }
1046
1079
 
1080
+ if (result && (method ?? spendMethod) === "link" && stepLinkSpendRequestId) {
1081
+ setPendingLinkSpendRequest(null);
1082
+ }
1083
+
1047
1084
  if (!result) {
1048
1085
  await recordStep(runState.run_id, step.id, {
1049
1086
  status: "skipped",
@@ -1172,9 +1209,6 @@ export function registerPlaybookTools(server: McpServer): void {
1172
1209
  });
1173
1210
  const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1174
1211
  pendingPlaybookRuns.delete(playbook.slug);
1175
- if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
1176
- setPendingLinkSpendRequest(null);
1177
- }
1178
1212
 
1179
1213
  const runBlocks = childOutputs.length > 0
1180
1214
  ? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)