@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.
@@ -1,12 +1,16 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
3
+ import { apiGet, apiPost, apiPostWithApprovedLinkSpendRequest, apiPostWithPayment } from "../core/api-client.js";
4
4
  import { uploadLocalFiles } from "../core/file-upload.js";
5
5
  import { hasWalletConfigured, getConfiguredMethods, getWalletAddress } from "../core/payments.js";
6
- import { requiresSpendConfirmation } from "../core/config.js";
6
+ import { getLinkConfig, setPendingLinkSpendRequest, requiresSpendConfirmation } from "../core/config.js";
7
7
  import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
8
8
  import { formatRunResult } from "../core/formatters.js";
9
- import { storeFeedbackToken } from "./_token-cache.js";
9
+ import {
10
+ decodeMppNetworkId,
11
+ ensureApprovedLinkSpendRequest,
12
+ LinkApprovalRequiredError,
13
+ } from "../core/link-cli.js";
10
14
  import {
11
15
  formatPaymentLabel,
12
16
  resolveConfirmationMethod,
@@ -33,6 +37,13 @@ type PlaybookStepQuote = {
33
37
  support_status: string;
34
38
  };
35
39
 
40
+ type PlaybookFanoutControl = {
41
+ key: string;
42
+ default: number;
43
+ max: number;
44
+ description: string;
45
+ };
46
+
36
47
  type PlaybookRecord = {
37
48
  id: string;
38
49
  slug: string;
@@ -44,6 +55,9 @@ type PlaybookRecord = {
44
55
  outcome: string;
45
56
  support_status: "supported" | "catalog_only";
46
57
  support_note?: string;
58
+ default_limits?: Record<string, number>;
59
+ applied_limits?: Record<string, number>;
60
+ fanout_controls?: PlaybookFanoutControl[];
47
61
  input_schema: Record<string, unknown>;
48
62
  sample_input: Record<string, unknown>;
49
63
  budget_notes: string[];
@@ -57,6 +71,9 @@ type PlaybookRecord = {
57
71
  estimated_cost_usd: number;
58
72
  step_count: number;
59
73
  blocking_issues: string[];
74
+ default_limits?: Record<string, number>;
75
+ applied_limits?: Record<string, number>;
76
+ fanout_controls?: PlaybookFanoutControl[];
60
77
  steps: PlaybookStepQuote[];
61
78
  };
62
79
  };
@@ -128,6 +145,35 @@ function money(value: number): string {
128
145
  return `$${value.toFixed(value < 1 ? 4 : 2)}`;
129
146
  }
130
147
 
148
+ function cents(valueUsd: number): string {
149
+ return String(Math.max(1, Math.ceil(valueUsd * 100)));
150
+ }
151
+
152
+ function nonEmptyLimits(limits?: Record<string, number>): Record<string, number> | undefined {
153
+ return limits && Object.keys(limits).length > 0 ? limits : undefined;
154
+ }
155
+
156
+ function appliedPlaybookLimits(playbook: PlaybookRecord): Record<string, number> | undefined {
157
+ return nonEmptyLimits(playbook.current_quote.applied_limits ?? playbook.applied_limits ?? playbook.default_limits);
158
+ }
159
+
160
+ function fanoutControls(playbook: PlaybookRecord): PlaybookFanoutControl[] {
161
+ return playbook.current_quote.fanout_controls ?? playbook.fanout_controls ?? [];
162
+ }
163
+
164
+ function buildPlaybookLinkApprovalContext(params: {
165
+ playbook: PlaybookRecord;
166
+ budget: number;
167
+ stepCount: number;
168
+ runId: string;
169
+ }): string {
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.",
174
+ ].join(" ");
175
+ }
176
+
131
177
  async function collectWalletAddresses(paymentMethod?: string): Promise<string[]> {
132
178
  const addresses = new Set<string>();
133
179
  const primary = await getWalletAddress(paymentMethod);
@@ -180,11 +226,13 @@ function formatPlaybookList(playbooks: PlaybookRecord[], query?: string) {
180
226
  }
181
227
 
182
228
  const lines = playbooks.map((playbook) => {
183
- const support = playbook.support_status === "supported" ? "supported" : `catalog-only: ${playbook.support_note ?? "not launch executable"}`;
229
+ const support = playbook.support_status === "supported" ? "supported" : `catalog-only: ${playbook.support_note ?? "not launch executable"}`;
230
+ const limits = appliedPlaybookLimits(playbook);
231
+ const fanout = limits ? ` · default limits ${JSON.stringify(limits)}` : "";
184
232
  return [
185
233
  ` ${playbook.name} (${playbook.slug})`,
186
234
  ` ${playbook.outcome}`,
187
- ` ${playbook.current_quote.step_count} step${playbook.current_quote.step_count === 1 ? "" : "s"} · ${money(playbook.current_quote.estimated_cost_usd)} est. · ${support}`,
235
+ ` ${playbook.current_quote.step_count} step${playbook.current_quote.step_count === 1 ? "" : "s"} · ${money(playbook.current_quote.estimated_cost_usd)} est. · ${support}${fanout}`,
188
236
  ` Favorites: ${playbook.stats?.favorite_count ?? 0} · Rating: ${playbook.stats?.average_rating ? `${playbook.stats.average_rating}/5` : "unrated"} (${playbook.stats?.rating_count ?? 0})`,
189
237
  ` Inspect: get_playbook({ slug: "${playbook.slug}" })`,
190
238
  ].join("\n");
@@ -228,14 +276,25 @@ function formatPlaybook(playbook: PlaybookRecord) {
228
276
  }
229
277
 
230
278
  if (playbook.budget_notes.length) lines.push("", "Budget notes:", ...playbook.budget_notes.map((note) => ` - ${note}`));
279
+ const limits = appliedPlaybookLimits(playbook);
280
+ const controls = fanoutControls(playbook);
281
+ if (controls.length) {
282
+ lines.push("", "Fanout controls:");
283
+ for (const control of controls) {
284
+ const current = limits?.[control.key] ?? control.default;
285
+ lines.push(` - ${control.key}: default ${control.default}, current ${current}, max ${control.max} — ${control.description}`);
286
+ }
287
+ if (limits) lines.push(` Override by passing limits, for example limits: ${JSON.stringify(limits)}`);
288
+ }
231
289
  if (playbook.risks.length) lines.push("", "Risks:", ...playbook.risks.map((risk) => ` - ${risk}`));
232
290
 
291
+ const runLimits = formatLimitsField(limits);
233
292
  lines.push(
234
293
  "",
235
294
  "Sample input:",
236
295
  JSON.stringify(playbook.sample_input, null, 2),
237
296
  "",
238
- `Run quote: run_playbook({ slug: "${playbook.slug}", input: <input>, budget: ${Math.max(1, Math.ceil(q.estimated_cost_usd + 1))} })`,
297
+ `Run quote: run_playbook({ slug: "${playbook.slug}", input: <input>, budget: ${Math.max(1, Math.ceil(q.estimated_cost_usd + 1))}${runLimits ? `, ${runLimits}` : ""} })`,
239
298
  );
240
299
  return lines.join("\n");
241
300
  }
@@ -260,6 +319,18 @@ function formatRunConfirmationCommand(slug: string, method: string | undefined,
260
319
 
261
320
  function formatQuoteNotes(playbook: PlaybookRecord): string[] {
262
321
  const lines: string[] = [];
322
+ const controls = fanoutControls(playbook);
323
+ if (controls.length > 0) {
324
+ const limits = appliedPlaybookLimits(playbook) ?? {};
325
+ lines.push("", "Fanout controls:");
326
+ for (const control of controls) {
327
+ const current = limits[control.key] ?? control.default;
328
+ lines.push(` - ${control.key}: current ${current}, default ${control.default}, max ${control.max}`);
329
+ }
330
+ if (Object.keys(limits).length > 0) {
331
+ lines.push(` Override by editing limits in the confirmation call: ${JSON.stringify(limits)}`);
332
+ }
333
+ }
263
334
  if (playbook.budget_notes.length > 0) {
264
335
  lines.push("", "Budget and fanout notes:", ...playbook.budget_notes.map((note) => ` - ${note}`));
265
336
  }
@@ -356,6 +427,39 @@ function stepInput(baseInput: Record<string, unknown>, step: PlaybookStepQuote,
356
427
  if (step.agent_slug === "scan-contract-for-risks" && !("file" in input) && input.contract) {
357
428
  input.file = input.contract;
358
429
  }
430
+ if (step.agent_slug === "marketing-copy-council" && !("brief" in input)) {
431
+ input.brief = input.product ?? input.context ?? input.text;
432
+ }
433
+ if (step.agent_slug === "write-landing-page-copy") {
434
+ const previousOutputs = Array.isArray(input.previous_outputs) ? input.previous_outputs : [];
435
+ const councilOutput = previousOutputs.find((item) => {
436
+ const record = item && typeof item === "object" ? item as Record<string, unknown> : null;
437
+ return record?.step === "council" || record?.step === "marketing-copy-council";
438
+ }) ?? previousOutputs[previousOutputs.length - 1];
439
+ const context = {
440
+ brief: input.brief,
441
+ product_slug: input.product_slug,
442
+ council_output: councilOutput,
443
+ previous_outputs: previousOutputs,
444
+ };
445
+ input.product = input.product ?? input.product_name ?? input.product_slug ?? "Landing page";
446
+ input.context = input.context ?? JSON.stringify(context);
447
+ }
448
+ if (step.agent_slug === "publish-html-to-a-public-url") {
449
+ const previousOutputs = Array.isArray(input.previous_outputs) ? input.previous_outputs : [];
450
+ const writerOutput = previousOutputs[previousOutputs.length - 1];
451
+ const outputRecord = writerOutput && typeof writerOutput === "object"
452
+ ? writerOutput as Record<string, unknown>
453
+ : null;
454
+ const output = outputRecord?.output && typeof outputRecord.output === "object"
455
+ ? outputRecord.output as Record<string, unknown>
456
+ : outputRecord?.output;
457
+ input.html = input.html
458
+ ?? (output && typeof output === "object" ? (output as Record<string, unknown>).html : undefined)
459
+ ?? (typeof output === "string" ? output : undefined)
460
+ ?? JSON.stringify(output ?? writerOutput ?? {});
461
+ input.slug = input.slug ?? input.product_slug;
462
+ }
359
463
  if (step.agent_slug === "place-search") {
360
464
  input.location = input.location ?? input.zip;
361
465
  }
@@ -660,7 +764,9 @@ export function registerPlaybookTools(server: McpServer): void {
660
764
  budget: effectiveBudget,
661
765
  limits: effectiveLimits,
662
766
  });
663
- playbook = quoted.playbook;
767
+ if (quoted?.playbook) {
768
+ playbook = quoted.playbook;
769
+ }
664
770
  } catch {
665
771
  // Quote analytics should not block the user-facing quote.
666
772
  }
@@ -673,12 +779,13 @@ export function registerPlaybookTools(server: McpServer): void {
673
779
  return text(`Cannot run ${playbook.slug}; child-agent issues must be fixed first:\n${playbook.current_quote.blocking_issues.map((issue) => `- ${issue}`).join("\n")}`);
674
780
  }
675
781
 
782
+ const runLimits = appliedPlaybookLimits(playbook) ?? effectiveLimits;
676
783
  const estimatedCost = playbook.current_quote.estimated_cost_usd;
677
784
  const spendCheck = canSpend({ method: spendMethod, amountUsd: Math.min(effectiveBudget, estimatedCost) });
678
785
  if (!spendCheck.ok) return text(spendCheck.message);
679
786
 
680
787
  if (estimatedCost > effectiveBudget) {
681
- pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: effectiveLimits });
788
+ pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: runLimits });
682
789
  const cheaperAlternatives = await formatCheaperAlternatives(playbook.slug, effectiveBudget);
683
790
  return text([
684
791
  `${playbook.name} quote exceeds the budget.`,
@@ -690,12 +797,12 @@ export function registerPlaybookTools(server: McpServer): void {
690
797
  ...(cheaperAlternatives ? ["", cheaperAlternatives] : []),
691
798
  "",
692
799
  `Try a higher budget or reduced limits, then call:`,
693
- formatRunConfirmationCommand(playbook.slug, method, Math.ceil(estimatedCost + 1), effectiveLimits),
800
+ formatRunConfirmationCommand(playbook.slug, method, Math.ceil(estimatedCost + 1), runLimits),
694
801
  ].join("\n"));
695
802
  }
696
803
 
697
804
  if ((requiresSpendConfirmation() || requiresPolicyConfirmation(spendMethod, estimatedCost)) && !confirmed) {
698
- pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: effectiveLimits });
805
+ pendingPlaybookRuns.set(playbook.slug, { playbook, input: effectiveInput, budget: effectiveBudget, method, limits: runLimits });
699
806
  return text([
700
807
  `Ready to run ${playbook.name}`,
701
808
  "",
@@ -711,7 +818,7 @@ export function registerPlaybookTools(server: McpServer): void {
711
818
  ...formatQuoteNotes(playbook),
712
819
  "",
713
820
  "To proceed, call:",
714
- formatRunConfirmationCommand(playbook.slug, method, effectiveBudget, effectiveLimits),
821
+ formatRunConfirmationCommand(playbook.slug, method, effectiveBudget, runLimits),
715
822
  "",
716
823
  "To cancel, do nothing.",
717
824
  ].join("\n"));
@@ -762,7 +869,7 @@ export function registerPlaybookTools(server: McpServer): void {
762
869
  slug: playbook.slug,
763
870
  input: processedInput,
764
871
  budget: effectiveBudget,
765
- limits: effectiveLimits,
872
+ limits: runLimits,
766
873
  }),
767
874
  completedStepIds: new Set<string>(),
768
875
  existingOutputs: [] as Array<{ step: string; job_id?: string; output?: unknown }>,
@@ -773,6 +880,14 @@ export function registerPlaybookTools(server: McpServer): void {
773
880
 
774
881
  let charged = runState.chargedUsd;
775
882
  const childOutputs: Array<{ step: string; job_id?: string; output?: unknown }> = [...runState.existingOutputs];
883
+ let linkSpendRequestId: string | undefined;
884
+ 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
+ });
776
891
 
777
892
  for (const [iteration, step] of runState.steps.entries()) {
778
893
  if (runState.completedStepIds.has(step.id)) {
@@ -824,19 +939,44 @@ export function registerPlaybookTools(server: McpServer): void {
824
939
  let schemaError: { status?: number; message?: string } | undefined;
825
940
  let usedPaidMethod = false;
826
941
  const executeChild = async (childInput: Record<string, unknown>) => {
942
+ const body = { input: childInput, playbook_context: playbookContext };
827
943
  try {
828
944
  return await apiPost<Record<string, unknown>>(
829
945
  `/agents/${step.agent_id}/run`,
830
- { input: childInput, playbook_context: playbookContext },
946
+ body,
831
947
  { ensureConsumerPrincipal: true },
832
948
  );
833
949
  } catch (err) {
834
950
  const status = (err as { status?: number })?.status;
835
951
  if (status !== 402) throw err;
836
952
  usedPaidMethod = true;
953
+ if ((method ?? spendMethod) === "link") {
954
+ const linkConfig = getLinkConfig();
955
+ if (!linkConfig) {
956
+ throw new Error('Payment method "link" is not configured. Run wallet_setup({ action: "add-link" }).');
957
+ }
958
+ const challenge = (err as { headers?: Headers })?.headers?.get("www-authenticate");
959
+ if (!challenge && !linkNetworkId) {
960
+ throw new Error("Link payment challenge did not include a WWW-Authenticate header.");
961
+ }
962
+ linkNetworkId = linkNetworkId ?? await decodeMppNetworkId(challenge!);
963
+ linkSpendRequestId = linkSpendRequestId ?? await ensureApprovedLinkSpendRequest({
964
+ amount: cents(effectiveBudget),
965
+ currency: "usd",
966
+ context: linkApprovalContext,
967
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
968
+ networkId: linkNetworkId,
969
+ paymentMethodId: linkConfig.paymentMethodId,
970
+ });
971
+ return await apiPostWithApprovedLinkSpendRequest<Record<string, unknown>>(
972
+ `/agents/${step.agent_id}/run`,
973
+ body,
974
+ linkSpendRequestId,
975
+ );
976
+ }
837
977
  return await apiPostWithPayment<Record<string, unknown>>(
838
978
  `/agents/${step.agent_id}/run`,
839
- { input: childInput, playbook_context: playbookContext },
979
+ body,
840
980
  method,
841
981
  );
842
982
  }
@@ -844,9 +984,26 @@ export function registerPlaybookTools(server: McpServer): void {
844
984
  try {
845
985
  result = await executeChild(payload);
846
986
  } catch (err) {
987
+ if (err instanceof LinkApprovalRequiredError) {
988
+ await recordStep(runState.run_id, step.id, {
989
+ status: "pending",
990
+ agent_id: step.agent_id,
991
+ provider_id: step.provider_id,
992
+ consumption_mode: "not_charged",
993
+ error_code: null,
994
+ failure_message: null,
995
+ });
996
+ await updateRun(runState.run_id, {
997
+ status: "paused",
998
+ error_code: "LINK_APPROVAL_REQUIRED",
999
+ failure_message: "Approve the Link spend request, then resume the playbook run.",
1000
+ });
1001
+ const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1002
+ return text(`${formatReceipt(receipt)}\n\n${err.message}`);
1003
+ }
847
1004
  const apiErr = err as { status?: number; message?: string };
848
- const validationFailed = apiErr?.status === 400
849
- && /missing required field|validation failed|invalid input|schema/i.test(apiErr.message ?? "");
1005
+ const validationFailed = (apiErr?.status === 400 || apiErr?.status === 422)
1006
+ && /missing required field|validation failed|invalid input|schema|provide at least one/i.test(apiErr.message ?? "");
850
1007
  if (validationFailed) {
851
1008
  try {
852
1009
  const agent = await apiGet<AgentSchemaDetails>(`/agents/${step.agent_id}`);
@@ -906,15 +1063,10 @@ export function registerPlaybookTools(server: McpServer): void {
906
1063
  }
907
1064
 
908
1065
  const jobId = (result.job_id as string) ?? "";
909
- const resultAgentId = (result.agent_id as string) ?? step.agent_id;
910
1066
  const status = result.status as string;
911
1067
  let output = result.output;
912
1068
  let errorCode = result.error_code as string | undefined;
913
1069
 
914
- if (result.feedback_token && jobId) {
915
- storeFeedbackToken(jobId, result.feedback_token as string, resultAgentId);
916
- }
917
-
918
1070
  if (status === "processing" && jobId) {
919
1071
  const pollResult = await pollJobUntilDone(jobId, method);
920
1072
  output = pollResult.output;
@@ -1020,14 +1172,16 @@ export function registerPlaybookTools(server: McpServer): void {
1020
1172
  });
1021
1173
  const receipt = await apiGet<PlaybookRunReceipt>(`/playbook-runs/${runState.run_id}`);
1022
1174
  pendingPlaybookRuns.delete(playbook.slug);
1175
+ if ((method ?? spendMethod) === "link" && linkSpendRequestId) {
1176
+ setPendingLinkSpendRequest(null);
1177
+ }
1023
1178
 
1024
1179
  const runBlocks = childOutputs.length > 0
1025
1180
  ? childOutputs.map((item) => item.output ? formatRunResult({ status: "success", job_id: item.job_id, output: item.output }, { paymentMethod: method }) : "").filter(Boolean)
1026
1181
  : [];
1027
1182
  return multiText(
1028
1183
  uploadSummary ? `${uploadSummary}\n\n${formatReceipt(receipt)}` : formatReceipt(receipt),
1029
- ...runBlocks,
1030
- `IMPORTANT: After presenting the result above, you MUST ask the user: "Paid ${money(receipt.charged_usd)} across ${receipt.steps.filter((s) => s.status === "succeeded").length} steps. Was this playbook useful? I can save it with favorite_playbook({ slug: "${playbook.slug}" }) or record feedback with rate_playbook({ slug: "${playbook.slug}", run_id: "${receipt.run_id}", rating: <1-5>, useful: true, comment: "..." })."`,
1184
+ ...runBlocks
1031
1185
  );
1032
1186
  },
1033
1187
  );
@@ -1053,39 +1207,6 @@ export function registerPlaybookTools(server: McpServer): void {
1053
1207
  },
1054
1208
  );
1055
1209
 
1056
- server.tool(
1057
- "rate_playbook",
1058
- "Record feedback on an Agent Wonderland Playbook after a run. Provide a rating, useful flag, comment, or any combination.",
1059
- {
1060
- playbook_id: z.string().optional(),
1061
- slug: z.string().optional(),
1062
- run_id: z.string().optional(),
1063
- rating: z.number().int().min(1).max(5).optional(),
1064
- useful: z.boolean().optional(),
1065
- comment: z.string().max(2000).optional(),
1066
- },
1067
- async ({ playbook_id, slug, run_id, rating, useful, comment }) => {
1068
- const id = slug ?? playbook_id;
1069
- if (!id) return text("Provide slug or playbook_id.");
1070
- if (rating == null && useful == null && !comment?.trim()) {
1071
- return text("Provide rating, useful, or comment.");
1072
- }
1073
-
1074
- const result = await apiPost<{ ok: boolean; feedback_id: string; playbook_slug: string }>(
1075
- `/playbooks/${encodeURIComponent(id)}/feedback`,
1076
- {
1077
- run_id,
1078
- rating,
1079
- useful,
1080
- comment,
1081
- },
1082
- { ensureConsumerPrincipal: true },
1083
- );
1084
-
1085
- return text(`Recorded feedback for ${result.playbook_slug}.`);
1086
- },
1087
- );
1088
-
1089
1210
  server.tool(
1090
1211
  "get_playbook_run",
1091
1212
  "Inspect current or historical Agent Wonderland Playbook run state, including completed steps, charged/refunded amount, partial output, and resume command when paused.",