@agentwonderland/mcp 0.1.45 → 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.
- package/dist/core/__tests__/payments.test.js +55 -1
- package/dist/core/__tests__/principal.test.js +18 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +37 -0
- package/dist/core/link-cli.d.ts +27 -0
- package/dist/core/link-cli.js +259 -0
- package/dist/core/mpp-client.d.ts +13 -1
- package/dist/core/mpp-client.js +45 -2
- package/dist/core/payments.js +135 -10
- package/dist/core/principal.js +2 -2
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +6 -4
- package/dist/tools/__tests__/search.test.d.ts +1 -0
- package/dist/tools/__tests__/search.test.js +66 -0
- package/dist/tools/__tests__/wallet.test.js +153 -0
- package/dist/tools/passes.js +1 -1
- package/dist/tools/run.js +1 -1
- package/dist/tools/search.js +33 -0
- package/dist/tools/solve.js +1 -1
- package/dist/tools/wallet.js +195 -7
- package/package.json +1 -1
- package/src/core/__tests__/payments.test.ts +78 -1
- package/src/core/__tests__/principal.test.ts +23 -0
- package/src/core/config.ts +56 -0
- package/src/core/link-cli.ts +300 -0
- package/src/core/mpp-client.ts +69 -2
- package/src/core/payments.ts +153 -11
- package/src/core/principal.ts +2 -2
- package/src/core/version.ts +1 -1
- package/src/index.ts +6 -4
- package/src/tools/__tests__/search.test.ts +78 -0
- package/src/tools/__tests__/wallet.test.ts +190 -0
- package/src/tools/passes.ts +1 -1
- package/src/tools/run.ts +1 -1
- package/src/tools/search.ts +40 -0
- package/src/tools/solve.ts +1 -1
- package/src/tools/wallet.ts +229 -6
|
@@ -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
|
+
}
|
package/src/core/mpp-client.ts
CHANGED
|
@@ -93,8 +93,75 @@ export const Mppx = {
|
|
|
93
93
|
},
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
export function stripe(
|
|
97
|
-
|
|
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 {
|
package/src/core/payments.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
|
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) =>
|
|
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;
|
package/src/core/principal.ts
CHANGED
|
@@ -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.",
|
package/src/core/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
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
|
|
55
|
-
"- Supported rails: Tempo USDC, Base USDC, Solana USDC
|
|
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
|
|
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
|
},
|