@agentwonderland/mcp 0.1.46 → 0.1.47

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.
@@ -0,0 +1,300 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+ const LINK_CLI_PACKAGE = "@stripe/link-cli";
6
+ const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
7
+
8
+ function sleep(ms: number): Promise<void> {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+
12
+ export interface LinkCliAuthStatus {
13
+ authenticated: boolean;
14
+ credentialsPath?: string;
15
+ pending?: boolean;
16
+ }
17
+
18
+ export interface LinkCliPaymentMethod {
19
+ id: string;
20
+ label?: string;
21
+ searchText?: string;
22
+ }
23
+
24
+ export interface LinkCliLogin {
25
+ verificationUrl: string;
26
+ phrase: string;
27
+ instruction?: string;
28
+ }
29
+
30
+ async function runLinkCli(args: string[], timeout = LINK_CLI_TIMEOUT_MS): Promise<unknown> {
31
+ try {
32
+ const { stdout } = await execFileAsync(
33
+ "npx",
34
+ ["--yes", LINK_CLI_PACKAGE, ...args, "--format", "json"],
35
+ {
36
+ maxBuffer: 1024 * 1024,
37
+ timeout,
38
+ },
39
+ );
40
+ const trimmed = stdout.trim();
41
+ return trimmed ? JSON.parse(trimmed) : null;
42
+ } catch (err) {
43
+ const error = err as Error & { stdout?: string; stderr?: string };
44
+ const output = error.stdout?.trim() || error.stderr?.trim();
45
+ if (output) {
46
+ try {
47
+ const parsed = JSON.parse(output) as { message?: string; code?: string };
48
+ const message = parsed.message ?? output;
49
+ throw new Error(parsed.code ? `${parsed.code}: ${message}` : message);
50
+ } catch (parseErr) {
51
+ if (parseErr instanceof SyntaxError) {
52
+ throw new Error(output);
53
+ }
54
+ throw parseErr;
55
+ }
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ function asRecord(value: unknown): Record<string, unknown> | null {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value)
63
+ ? value as Record<string, unknown>
64
+ : null;
65
+ }
66
+
67
+ function walk(value: unknown, visit: (value: unknown, key?: string) => string | null, key?: string): string | null {
68
+ const direct = visit(value, key);
69
+ if (direct) return direct;
70
+
71
+ if (Array.isArray(value)) {
72
+ for (const item of value) {
73
+ const found = walk(item, visit);
74
+ if (found) return found;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ const record = asRecord(value);
80
+ if (record) {
81
+ for (const [childKey, childValue] of Object.entries(record)) {
82
+ const found = walk(childValue, visit, childKey);
83
+ if (found) return found;
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ function extractSharedPaymentToken(output: unknown): string | null {
91
+ return walk(output, (value, key) => {
92
+ if (typeof value !== "string") return null;
93
+ if (value.startsWith("spt_")) return value;
94
+ if (key && /shared.*payment.*token|spt/i.test(key) && value.includes("spt_")) {
95
+ return value.match(/spt_[A-Za-z0-9_]+/)?.[0] ?? null;
96
+ }
97
+ return null;
98
+ });
99
+ }
100
+
101
+ function extractSpendRequestApproval(output: unknown): { id: string; approvalUrl?: string; status?: string } | null {
102
+ const values = Array.isArray(output) ? output : [output];
103
+ for (const value of values) {
104
+ const record = asRecord(value);
105
+ if (!record) continue;
106
+ const id = typeof record.id === "string" && record.id.startsWith("lsrq_")
107
+ ? record.id
108
+ : null;
109
+ if (!id) continue;
110
+ return {
111
+ id,
112
+ approvalUrl: typeof record.approval_url === "string" ? record.approval_url : undefined,
113
+ status: typeof record.status === "string" ? record.status : undefined,
114
+ };
115
+ }
116
+ return null;
117
+ }
118
+
119
+ function normalizePaymentMethods(output: unknown): LinkCliPaymentMethod[] {
120
+ const values = Array.isArray(output)
121
+ ? output
122
+ : Array.isArray(asRecord(output)?.data)
123
+ ? asRecord(output)?.data as unknown[]
124
+ : Array.isArray(asRecord(output)?.payment_methods)
125
+ ? asRecord(output)?.payment_methods as unknown[]
126
+ : [];
127
+
128
+ return values
129
+ .map<LinkCliPaymentMethod | null>((value) => {
130
+ const record = asRecord(value);
131
+ if (!record) return null;
132
+ const id = typeof record.id === "string"
133
+ ? record.id
134
+ : typeof record.payment_method_id === "string"
135
+ ? record.payment_method_id
136
+ : null;
137
+ if (!id) return null;
138
+ const name = typeof record.name === "string" ? record.name : undefined;
139
+ const type = typeof record.type === "string" ? record.type : undefined;
140
+ const cardDetails = asRecord(record.card_details);
141
+ const bankDetails = asRecord(record.bank_account_details);
142
+ const brand = typeof cardDetails?.brand === "string"
143
+ ? cardDetails.brand
144
+ : typeof record.brand === "string"
145
+ ? record.brand
146
+ : undefined;
147
+ const last4 = typeof cardDetails?.last4 === "string"
148
+ ? cardDetails.last4
149
+ : typeof bankDetails?.last4 === "string"
150
+ ? bankDetails.last4
151
+ : typeof record.last4 === "string"
152
+ ? record.last4
153
+ : undefined;
154
+ const bankName = typeof bankDetails?.bank_name === "string" ? bankDetails.bank_name : undefined;
155
+ const typeLabel = type === "BANK_ACCOUNT" ? "bank" : type === "CARD" ? "card" : type?.toLowerCase();
156
+ const labelParts = [
157
+ name ?? bankName ?? brand ?? typeLabel,
158
+ brand && name?.toLowerCase().includes(brand.toLowerCase()) !== true ? brand : undefined,
159
+ last4 ? `****${last4}` : undefined,
160
+ ];
161
+ const label = labelParts.filter(Boolean).join(" ");
162
+ const searchText = [id, label, name, type, brand, bankName, last4].filter(Boolean).join(" ").toLowerCase();
163
+ return { id, ...(label ? { label } : {}), searchText };
164
+ })
165
+ .filter((value): value is LinkCliPaymentMethod => Boolean(value));
166
+ }
167
+
168
+ export async function getLinkCliAuthStatus(): Promise<LinkCliAuthStatus> {
169
+ try {
170
+ const output = await runLinkCli(["auth", "status"], 30_000);
171
+ const status = Array.isArray(output) ? asRecord(output[0]) : asRecord(output);
172
+ return {
173
+ authenticated: status?.authenticated === true,
174
+ credentialsPath: typeof status?.credentials_path === "string" ? status.credentials_path : undefined,
175
+ pending: status?.pending === true,
176
+ };
177
+ } catch {
178
+ return { authenticated: false };
179
+ }
180
+ }
181
+
182
+ export async function startLinkCliLogin(): Promise<LinkCliLogin> {
183
+ const output = await runLinkCli(["auth", "login", "--client-name", "Agent Wonderland MCP"]);
184
+ const login = Array.isArray(output) ? asRecord(output[0]) : asRecord(output);
185
+ const verificationUrl = typeof login?.verification_url === "string"
186
+ ? login.verification_url
187
+ : typeof login?.verificationUrl === "string"
188
+ ? login.verificationUrl
189
+ : null;
190
+ const phrase = typeof login?.phrase === "string" ? login.phrase : null;
191
+ if (!verificationUrl || !phrase) {
192
+ throw new Error("Link CLI did not return a verification URL.");
193
+ }
194
+ return {
195
+ verificationUrl,
196
+ phrase,
197
+ instruction: typeof login?.instruction === "string" ? login.instruction : undefined,
198
+ };
199
+ }
200
+
201
+ export async function openLinkPaymentMethodAdd(): Promise<void> {
202
+ await runLinkCli(["payment-methods", "add"]);
203
+ }
204
+
205
+ export async function listLinkPaymentMethods(): Promise<LinkCliPaymentMethod[]> {
206
+ const output = await runLinkCli(["payment-methods", "list"], 60_000);
207
+ return normalizePaymentMethods(output);
208
+ }
209
+
210
+ export async function createLinkSharedPaymentToken(params: {
211
+ amount: string;
212
+ currency: string;
213
+ context: string;
214
+ expiresAt: number;
215
+ networkId: string;
216
+ paymentMethodId: string;
217
+ }): Promise<string> {
218
+ const args = [
219
+ "spend-request",
220
+ "create",
221
+ "--credential-type",
222
+ "shared_payment_token",
223
+ "--network-id",
224
+ params.networkId,
225
+ "--amount",
226
+ params.amount,
227
+ "--currency",
228
+ params.currency,
229
+ "--payment-method-id",
230
+ params.paymentMethodId,
231
+ "--context",
232
+ params.context,
233
+ "--request-approval",
234
+ ];
235
+
236
+ if (process.env.AGENTWONDERLAND_LINK_TEST_MODE === "1") {
237
+ args.push("--test");
238
+ }
239
+
240
+ let output: unknown;
241
+ try {
242
+ output = await runLinkCli(args);
243
+ } catch (err) {
244
+ const message = err instanceof Error ? err.message : String(err);
245
+ if (/invalid network_id|could not retrieve merchant information/i.test(message)) {
246
+ throw new Error(
247
+ [
248
+ message,
249
+ "",
250
+ `Link CLI rejected the merchant network_id "${params.networkId}".`,
251
+ "For local Agent Wonderland testing, restart the gateway with a Stripe key whose live/test mode matches STRIPE_PROFILE_ID. If the modes already match, the Stripe profile likely is not provisioned for Link Agentic Commerce/SPT yet or Stripe needs to provide a different network id.",
252
+ ].join("\n"),
253
+ );
254
+ }
255
+ throw err;
256
+ }
257
+ const spt = extractSharedPaymentToken(output);
258
+ if (spt) {
259
+ return spt;
260
+ }
261
+
262
+ const approval = extractSpendRequestApproval(output);
263
+ if (approval?.id && approval.status === "pending_approval") {
264
+ if (approval.approvalUrl) {
265
+ console.error(`Link approval required: ${approval.approvalUrl}`);
266
+ }
267
+ let retrieved = await runLinkCli([
268
+ "spend-request",
269
+ "retrieve",
270
+ approval.id,
271
+ "--interval",
272
+ "2",
273
+ "--max-attempts",
274
+ "150",
275
+ ]);
276
+ let retrievedSpt = extractSharedPaymentToken(retrieved);
277
+ for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
278
+ await sleep(2_000);
279
+ retrieved = await runLinkCli([
280
+ "spend-request",
281
+ "retrieve",
282
+ approval.id,
283
+ ]);
284
+ retrievedSpt = extractSharedPaymentToken(retrieved);
285
+ }
286
+ if (retrievedSpt) {
287
+ return retrievedSpt;
288
+ }
289
+ throw new Error(
290
+ [
291
+ "Link spend request finished without a shared payment token.",
292
+ approval.approvalUrl ? `Approval URL: ${approval.approvalUrl}` : undefined,
293
+ ].filter(Boolean).join("\n"),
294
+ );
295
+ }
296
+
297
+ {
298
+ throw new Error("Link spend request completed without a shared payment token in the CLI response.");
299
+ }
300
+ }
@@ -93,8 +93,75 @@ export const Mppx = {
93
93
  },
94
94
  };
95
95
 
96
- export function stripe(_parameters?: unknown): ClientMethod {
97
- throw new Error("Stripe card payments are temporarily unavailable.");
96
+ export function stripe(parameters: {
97
+ createToken: (parameters: {
98
+ amount: string;
99
+ challenge: Challenge;
100
+ currency: string;
101
+ expiresAt: number;
102
+ metadata?: Record<string, string>;
103
+ networkId: string;
104
+ paymentMethod?: string;
105
+ }) => Promise<string>;
106
+ externalId?: string;
107
+ paymentMethod?: string;
108
+ }): ClientMethod[] {
109
+ const { createToken, externalId, paymentMethod: defaultPaymentMethod } = parameters;
110
+
111
+ return [
112
+ Method.toClient(
113
+ {
114
+ name: "stripe",
115
+ intent: "charge",
116
+ },
117
+ {
118
+ async createCredential({ challenge }) {
119
+ const paymentMethod = defaultPaymentMethod;
120
+ if (!paymentMethod) {
121
+ throw new Error("paymentMethod is required");
122
+ }
123
+
124
+ const amount = String(challenge.request.amount ?? "");
125
+ const currency = String(challenge.request.currency ?? "");
126
+ const methodDetails = challenge.request.methodDetails as {
127
+ networkId?: string;
128
+ metadata?: Record<string, string>;
129
+ } | undefined;
130
+ const networkId = methodDetails?.networkId;
131
+ if (!networkId) {
132
+ throw new Error("networkId is required in stripe payment challenge");
133
+ }
134
+
135
+ const metadata = methodDetails?.metadata;
136
+ if (metadata?.externalId) {
137
+ throw new Error("methodDetails.metadata.externalId is reserved");
138
+ }
139
+
140
+ const expiresAt = challenge.expires
141
+ ? Math.floor(new Date(challenge.expires).getTime() / 1000)
142
+ : Math.floor(Date.now() / 1000) + 3600;
143
+
144
+ const spt = await createToken({
145
+ amount,
146
+ challenge,
147
+ currency,
148
+ expiresAt,
149
+ metadata,
150
+ networkId,
151
+ paymentMethod,
152
+ });
153
+
154
+ return Credential.serialize({
155
+ challenge,
156
+ payload: {
157
+ spt,
158
+ ...(externalId ? { externalId } : {}),
159
+ },
160
+ });
161
+ },
162
+ },
163
+ ),
164
+ ];
98
165
  }
99
166
 
100
167
  function createPaymentFetch(baseFetch: typeof fetch, methods: ClientMethod[]): typeof fetch {
@@ -15,6 +15,7 @@ import {
15
15
  getWallets,
16
16
  getDefaultWallet,
17
17
  getCardConfig,
18
+ getLinkConfig,
18
19
  resolveWalletAndChain,
19
20
  getApiUrl,
20
21
  type WalletEntry,
@@ -37,6 +38,25 @@ const REGISTRY_METHOD_MAP: Record<string, string> = {
37
38
  base: "base_usdc",
38
39
  solana: "solana_usdc",
39
40
  card: "stripe_card",
41
+ link: "stripe_card",
42
+ };
43
+
44
+ const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 10_000;
45
+
46
+ const ACCEPTED_PAYMENT_ALIASES: Record<string, string[]> = {
47
+ tempo: ["tempo_usdc", "tempo"],
48
+ base: ["base_usdc", "base"],
49
+ solana: ["solana_usdc", "solana"],
50
+ card: ["stripe_card", "card"],
51
+ link: ["stripe_card", "card", "link"],
52
+ };
53
+
54
+ const DISCOVERY_PAYMENT_ALIASES: Record<string, string[]> = {
55
+ tempo: ["tempo_usdc"],
56
+ base: ["base_usdc"],
57
+ solana: ["solana_usdc"],
58
+ card: ["stripe_card", "card"],
59
+ link: ["stripe_card", "card"],
40
60
  };
41
61
 
42
62
  const METHOD_REGISTRY_MAP: Record<string, string> = {
@@ -44,6 +64,8 @@ const METHOD_REGISTRY_MAP: Record<string, string> = {
44
64
  base_usdc: "base",
45
65
  solana_usdc: "solana",
46
66
  stripe_card: "card",
67
+ card: "card",
68
+ link: "link",
47
69
  };
48
70
 
49
71
  // ── Helpers ─────────────────────────────────────────────────────
@@ -62,6 +84,12 @@ function cardCacheKey(): string | null {
62
84
  return `card:${getApiUrl()}:${card.consumerToken}:${card.paymentMethodId ?? ""}`;
63
85
  }
64
86
 
87
+ function linkCacheKey(): string | null {
88
+ const link = getLinkConfig();
89
+ if (!link) return null;
90
+ return `link:${getApiUrl()}:${link.paymentMethodId}`;
91
+ }
92
+
65
93
  function clearStaleCardCache(activeKey?: string): void {
66
94
  for (const key of fetchCache.keys()) {
67
95
  if (key.startsWith("card:") && key !== activeKey) {
@@ -70,6 +98,14 @@ function clearStaleCardCache(activeKey?: string): void {
70
98
  }
71
99
  }
72
100
 
101
+ function clearStaleLinkCache(activeKey?: string): void {
102
+ for (const key of fetchCache.keys()) {
103
+ if (key.startsWith("link:") && key !== activeKey) {
104
+ fetchCache.delete(key);
105
+ }
106
+ }
107
+ }
108
+
73
109
  // ── Per-protocol initializers ───────────────────────────────────
74
110
 
75
111
  async function initEvmMppForChain(
@@ -157,6 +193,76 @@ async function initCard(): Promise<typeof fetch | null> {
157
193
  }
158
194
  }
159
195
 
196
+ function formatMinorCurrencyAmount(currency: string, amount: string): string {
197
+ return `${currency.toUpperCase()} ${(Number(amount) / 100).toFixed(2)}`;
198
+ }
199
+
200
+ function getLinkApprovalLimitAmount(actualAmount: string): string {
201
+ const actualAmountCents = Number(actualAmount);
202
+ const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
203
+ const defaultLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
204
+ ? Math.floor(configuredLimit)
205
+ : DEFAULT_LINK_APPROVAL_LIMIT_CENTS;
206
+ return String(Math.max(actualAmountCents, defaultLimit));
207
+ }
208
+
209
+ function buildLinkApprovalContext(params: {
210
+ amount: string;
211
+ approvalAmount: string;
212
+ currency: string;
213
+ metadata?: Record<string, string>;
214
+ }): string {
215
+ const amountText = formatMinorCurrencyAmount(params.currency, params.amount);
216
+ const approvalAmountText = formatMinorCurrencyAmount(params.currency, params.approvalAmount);
217
+ const agent = params.metadata?.agent_id ? ` Agent ID: ${params.metadata.agent_id}.` : "";
218
+ const job = params.metadata?.job_id ? ` Job ID: ${params.metadata.job_id}.` : "";
219
+ return (
220
+ `Approve up to ${approvalAmountText} for Agent Wonderland machine payments. ` +
221
+ `This specific user-confirmed agent run is quoted at ${amountText}; the Agent Wonderland gateway will charge the exact quote for this request, not the full approval limit unless the quote itself is that amount.${agent}${job}`
222
+ );
223
+ }
224
+
225
+ async function initLink(): Promise<typeof fetch | null> {
226
+ const linkConfig = getLinkConfig();
227
+ if (!linkConfig) return null;
228
+
229
+ try {
230
+ const { Mppx, stripe } = await import("./mpp-client.js");
231
+ const { createLinkSharedPaymentToken } = await import("./link-cli.js");
232
+ const mppx = Mppx.create({
233
+ methods: [stripe({
234
+ paymentMethod: linkConfig.paymentMethodId,
235
+ createToken: async (params: {
236
+ amount: string;
237
+ currency: string;
238
+ networkId: string;
239
+ expiresAt: number;
240
+ metadata?: Record<string, string>;
241
+ }) => {
242
+ const approvalAmount = getLinkApprovalLimitAmount(params.amount);
243
+ return createLinkSharedPaymentToken({
244
+ amount: approvalAmount,
245
+ currency: params.currency,
246
+ context: buildLinkApprovalContext({
247
+ amount: params.amount,
248
+ approvalAmount,
249
+ currency: params.currency,
250
+ metadata: params.metadata,
251
+ }),
252
+ expiresAt: params.expiresAt,
253
+ networkId: params.networkId,
254
+ paymentMethodId: linkConfig.paymentMethodId,
255
+ });
256
+ },
257
+ })] as any,
258
+ polyfill: false,
259
+ });
260
+ return mppx.fetch.bind(mppx) as typeof fetch;
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
160
266
  /**
161
267
  * Initialize a payment-aware fetch for a given wallet + chain.
162
268
  */
@@ -184,6 +290,21 @@ function initFailureMessage(method: string, wallet: WalletEntry, chain: string,
184
290
  * @param method - wallet ID, chain name, or "card". Omit for auto-detection.
185
291
  */
186
292
  export async function getPaymentFetch(method?: string): Promise<typeof fetch> {
293
+ if (method === "link") {
294
+ const ck = linkCacheKey();
295
+ clearStaleLinkCache(ck ?? undefined);
296
+ if (!ck) {
297
+ throw new Error('Payment method "link" is not configured. Run wallet_setup({ action: "add-link" }) after logging into Link.');
298
+ }
299
+ if (fetchCache.has(ck)) return fetchCache.get(ck)!;
300
+ const pf = await initLink();
301
+ if (pf) {
302
+ fetchCache.set(ck, pf);
303
+ return pf;
304
+ }
305
+ throw new Error('Payment method "link" failed to initialize. Check Link CLI auth with: npx @stripe/link-cli auth status');
306
+ }
307
+
187
308
  // Card payment
188
309
  if (method === "card") {
189
310
  if (!ENABLE_CARD_PAYMENT) {
@@ -229,6 +350,24 @@ export async function getPaymentFetch(method?: string): Promise<typeof fetch> {
229
350
  const defaultMethod = getConfig().defaultPaymentMethod;
230
351
 
231
352
  for (const m of configured) {
353
+ if (m === "link") {
354
+ const ck = linkCacheKey();
355
+ clearStaleLinkCache(ck ?? undefined);
356
+ if (!ck) continue;
357
+ if (fetchCache.has(ck)) return fetchCache.get(ck)!;
358
+ const pf = await initLink();
359
+ if (pf) {
360
+ fetchCache.set(ck, pf);
361
+ return pf;
362
+ }
363
+ if (m === defaultMethod) {
364
+ const others = configured.filter((x) => x !== m);
365
+ const altText = others.length > 0 ? ` Available alternatives: ${others.join(", ")}` : "";
366
+ throw new Error(`Link payment failed to initialize. Check Link CLI auth with wallet_status.${altText}`);
367
+ }
368
+ continue;
369
+ }
370
+
232
371
  if (m === "card") {
233
372
  const ck = cardCacheKey();
234
373
  clearStaleCardCache(ck ?? undefined);
@@ -317,6 +456,9 @@ export function getConfiguredMethods(): string[] {
317
456
  if (ENABLE_CARD_PAYMENT && getCardConfig()) {
318
457
  methods.push("card");
319
458
  }
459
+ if (getLinkConfig()) {
460
+ methods.push("link");
461
+ }
320
462
 
321
463
  // Respect defaultPaymentMethod — move it to front of list (ignore card when disabled)
322
464
  const defaultMethod = getConfig().defaultPaymentMethod;
@@ -337,6 +479,7 @@ export function getConfiguredMethods(): string[] {
337
479
  */
338
480
  export function normalizePaymentMethod(method: string): string | null {
339
481
  if (method === "card") return "card";
482
+ if (method === "link") return "link";
340
483
  const resolved = resolveWalletAndChain(method);
341
484
  return resolved?.chain ?? null;
342
485
  }
@@ -350,6 +493,7 @@ export function paymentMethodDisplayName(method: string): string {
350
493
  case "base": return "Base USDC";
351
494
  case "solana": return "Solana USDC";
352
495
  case "card": return "Card";
496
+ case "link": return "Link";
353
497
  default: return method;
354
498
  }
355
499
  }
@@ -361,9 +505,8 @@ export function paymentMethodDisplayName(method: string): string {
361
505
  */
362
506
  export function getAcceptedPaymentMethods(): string[] {
363
507
  const methods = getConfiguredMethods();
364
- return methods
365
- .map((m) => REGISTRY_METHOD_MAP[m])
366
- .filter(Boolean);
508
+ return [...new Set(methods
509
+ .flatMap((m) => DISCOVERY_PAYMENT_ALIASES[m] ?? [REGISTRY_METHOD_MAP[m]].filter(Boolean)))];
367
510
  }
368
511
 
369
512
  export function toRegistryPaymentMethod(method: string): string | null {
@@ -381,20 +524,19 @@ export function getCompatiblePaymentMethods(
381
524
  return [...configuredMethods];
382
525
  }
383
526
 
384
- const acceptedMethods = new Set(
385
- acceptedPayments
386
- .map((payment) => METHOD_REGISTRY_MAP[payment])
387
- .filter(Boolean),
388
- );
527
+ const acceptedRegistryMethods = new Set(acceptedPayments);
389
528
 
390
- return configuredMethods.filter((method) => acceptedMethods.has(method));
529
+ return configuredMethods.filter((method) => {
530
+ const aliases = ACCEPTED_PAYMENT_ALIASES[method] ?? [REGISTRY_METHOD_MAP[method]].filter(Boolean);
531
+ return aliases.some((alias) => acceptedRegistryMethods.has(alias));
532
+ });
391
533
  }
392
534
 
393
535
  /**
394
536
  * Check whether any payment method is configured.
395
537
  */
396
538
  export function hasWalletConfigured(): boolean {
397
- return getWallets().length > 0 || getCardConfig() !== null;
539
+ return getWallets().length > 0 || getCardConfig() !== null || getLinkConfig() !== null;
398
540
  }
399
541
 
400
542
  /**
@@ -404,7 +546,7 @@ export async function getWalletAddress(method?: string): Promise<string | null>
404
546
  let chain: string | undefined;
405
547
  let wallet: WalletEntry | undefined;
406
548
 
407
- if (method && method !== "card") {
549
+ if (method && method !== "card" && method !== "link") {
408
550
  const resolved = resolveWalletAndChain(method);
409
551
  wallet = resolved?.wallet;
410
552
  chain = resolved?.chain;
@@ -132,7 +132,7 @@ export async function getBaseRebatePrincipal(): Promise<string | null> {
132
132
  }
133
133
 
134
134
  export async function getConsumerPrincipalForMethod(method?: string): Promise<string | null> {
135
- if (!method || method === "card") {
135
+ if (!method || method === "card" || method === "link") {
136
136
  return getConsumerPrincipal();
137
137
  }
138
138
 
@@ -158,7 +158,7 @@ export async function ensureConsumerPrincipalForMethod(method?: string): Promise
158
158
  const existing = await getConsumerPrincipalForMethod(method);
159
159
  if (existing) return existing;
160
160
 
161
- if (method && method !== "card") {
161
+ if (method && method !== "card" && method !== "link") {
162
162
  throw new Error(
163
163
  `Could not derive a consumer principal for payment method "${method}". ` +
164
164
  "Check wallet_status and confirm that chain is configured for the active wallet.",
@@ -1 +1 @@
1
- export const MCP_PACKAGE_VERSION = "0.1.44";
1
+ export const MCP_PACKAGE_VERSION = "0.1.47";
package/src/index.ts CHANGED
@@ -51,11 +51,12 @@ export async function startMcpServer(): Promise<void> {
51
51
  "3. After a successful run, rate_agent() and optionally tip_agent() if the result was useful.",
52
52
  "4. Use list_jobs() to recover state across sessions (it checks every configured wallet).",
53
53
  "",
54
- "PAYMENT (crypto-only right now):",
55
- "- Supported rails: Tempo USDC, Base USDC, Solana USDC. Card is temporarily disabled pending Stripe SPT approval.",
54
+ "PAYMENT:",
55
+ "- Supported rails: Tempo USDC, Base USDC, Solana USDC, and local Link SPT via @stripe/link-cli.",
56
+ "- Card is temporarily disabled pending Stripe SPT approval.",
56
57
  "- Tempo and Base share one EVM wallet key. Solana uses a separate ed25519 key. One OWS wallet can manage both.",
57
58
  "- If pay_with is omitted, the MCP auto-selects a compatible configured rail. Pass pay_with explicitly",
58
- " (tempo | base | solana | wallet-id) for deterministic behavior.",
59
+ " (tempo | base | solana | link | wallet-id) for deterministic behavior.",
59
60
  "- Payment is automatic: on a 402 challenge the MCP signs on-chain, submits, then retries. Failed runs are refunded.",
60
61
  "- If a specific rail fails, surface the real reason — do NOT silently retry with a different method.",
61
62
  "- Headless/automation: set wallet_set_policy() to cap max_per_tx and max_per_day so a runaway loop can't drain funds.",
@@ -70,7 +71,8 @@ export async function startMcpServer(): Promise<void> {
70
71
  "",
71
72
  "WALLET HYGIENE:",
72
73
  "- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
73
- "- To create or import a crypto wallet: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
74
+ "- To set up payments: wallet_setup({ action: \"start\" }). Link card/bank is recommended for most users.",
75
+ "- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
74
76
  "- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
75
77
  ].join("\n"),
76
78
  },