aixyz 0.33.0 → 0.35.0
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/accepts.ts +38 -23
- package/app/index.ts +44 -8
- package/app/payment/payment.ts +149 -12
- package/app/plugin.ts +26 -4
- package/app/plugins/a2a.ts +3 -2
- package/app/plugins/index-page/index.ts +3 -2
- package/app/plugins/mcp.ts +85 -18
- package/app/plugins/session/index.ts +193 -0
- package/app/plugins/session/memory.ts +206 -0
- package/app/types.ts +6 -2
- package/docs/api-reference/accepts.mdx +71 -5
- package/docs/api-reference/agent.mdx +1 -1
- package/docs/api-reference/aixyz-server.mdx +3 -0
- package/docs/api-reference/session-plugin.mdx +192 -0
- package/docs/api-reference/tools.mdx +4 -4
- package/docs/getting-started/agent-and-tools.mdx +11 -0
- package/docs/getting-started/payments.mdx +47 -0
- package/docs/getting-started/project-structure.mdx +8 -1
- package/docs/protocols/mcp.mdx +17 -0
- package/docs/templates/advanced/x402-sessions.mdx +99 -0
- package/docs/templates/overview.mdx +3 -0
- package/package.json +5 -4
package/accepts.ts
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { FacilitatorClient, HTTPFacilitatorClient } from "@x402/core/server";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
scheme: "exact";
|
|
8
|
-
price: string;
|
|
4
|
+
const AcceptsX402Scheme = z.object({
|
|
5
|
+
scheme: z.literal("exact"),
|
|
6
|
+
price: z.string(),
|
|
9
7
|
// TODO(kevin): update type to Network (`string:string`)
|
|
10
|
-
network
|
|
11
|
-
payTo
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
z.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
network: z.string().optional(),
|
|
9
|
+
payTo: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/** Used for multiple accepts — explicit network is required to properly register schemes on the resource server. */
|
|
13
|
+
const AcceptsX402EntryScheme = z.object({
|
|
14
|
+
scheme: z.literal("exact"),
|
|
15
|
+
price: z.string(),
|
|
16
|
+
network: z.string(),
|
|
17
|
+
payTo: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const AcceptsFreeScheme = z.object({
|
|
21
|
+
scheme: z.literal("free"),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type AcceptsX402 = z.infer<typeof AcceptsX402Scheme>;
|
|
25
|
+
export type AcceptsX402Entry = z.infer<typeof AcceptsX402EntryScheme>;
|
|
26
|
+
export type AcceptsX402Multi = AcceptsX402Entry[];
|
|
27
|
+
export type AcceptsFree = z.infer<typeof AcceptsFreeScheme>;
|
|
28
|
+
export type Accepts = AcceptsX402 | AcceptsFree | AcceptsX402Multi;
|
|
29
|
+
|
|
30
|
+
export const AcceptsScheme: z.ZodType<Accepts> = z.union([
|
|
31
|
+
z.discriminatedUnion("scheme", [AcceptsX402Scheme, AcceptsFreeScheme]),
|
|
32
|
+
z.array(AcceptsX402EntryScheme).min(1),
|
|
28
33
|
]);
|
|
29
34
|
|
|
35
|
+
export function normalizeAcceptsX402(accepts: AcceptsX402 | AcceptsX402Multi): AcceptsX402[] {
|
|
36
|
+
return Array.isArray(accepts) ? accepts : [accepts];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isAcceptsPaid(accepts: Accepts): accepts is AcceptsX402 | AcceptsX402Multi {
|
|
40
|
+
if (Array.isArray(accepts))
|
|
41
|
+
return accepts.length > 0 && accepts.every((e) => e.scheme === "exact" && typeof e.network === "string");
|
|
42
|
+
return accepts.scheme === "exact";
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
export type { FacilitatorClient };
|
|
31
46
|
|
|
32
47
|
export { HTTPFacilitatorClient };
|
package/app/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isAcceptsPaid, AcceptsScheme } from "../accepts";
|
|
2
|
+
import type { Accepts } from "../accepts";
|
|
2
3
|
import type { FacilitatorClient } from "@x402/core/server";
|
|
3
|
-
import { type HttpMethod, type RouteHandler, type Middleware, type RouteEntry } from "./types";
|
|
4
|
+
import { type HttpMethod, type RouteHandler, type Middleware, type RouteEntry, type RouteOptions } from "./types";
|
|
4
5
|
import { PaymentGateway } from "./payment/payment";
|
|
5
|
-
import { Network } from "@x402/core/types";
|
|
6
6
|
import { getAixyzConfig } from "@aixyz/config";
|
|
7
7
|
import { loadEnvConfig } from "@next/env";
|
|
8
|
-
import { BasePlugin, type RegisterContext, type InitializeContext } from "./plugin";
|
|
8
|
+
import { BasePlugin, type RegisterContext, type InitializeContext, type PaymentHookContext } from "./plugin";
|
|
9
9
|
|
|
10
10
|
// Load .env and .env.production files at runtime (local files are excluded at build time).
|
|
11
11
|
loadEnvConfig(process.cwd());
|
|
@@ -37,7 +37,6 @@ export class AixyzApp {
|
|
|
37
37
|
|
|
38
38
|
if (options?.facilitators) {
|
|
39
39
|
this.payment = new PaymentGateway(options.facilitators, config);
|
|
40
|
-
this.payment.register((config.x402.network as Network) ?? "eip155:8453");
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -69,6 +68,35 @@ export class AixyzApp {
|
|
|
69
68
|
// clear registered routes for this plugin before registration, in case of hot reload or multiple registrations
|
|
70
69
|
plugin.registeredRoutes.clear();
|
|
71
70
|
|
|
71
|
+
const paymentHooks: PaymentHookContext | undefined = this.payment
|
|
72
|
+
? {
|
|
73
|
+
onBeforeVerify: (h) => {
|
|
74
|
+
this.payment!.onBeforeVerify(h);
|
|
75
|
+
return paymentHooks!;
|
|
76
|
+
},
|
|
77
|
+
onAfterVerify: (h) => {
|
|
78
|
+
this.payment!.onAfterVerify(h);
|
|
79
|
+
return paymentHooks!;
|
|
80
|
+
},
|
|
81
|
+
onVerifyFailure: (h) => {
|
|
82
|
+
this.payment!.onVerifyFailure(h);
|
|
83
|
+
return paymentHooks!;
|
|
84
|
+
},
|
|
85
|
+
onBeforeSettle: (h) => {
|
|
86
|
+
this.payment!.onBeforeSettle(h);
|
|
87
|
+
return paymentHooks!;
|
|
88
|
+
},
|
|
89
|
+
onAfterSettle: (h) => {
|
|
90
|
+
this.payment!.onAfterSettle(h);
|
|
91
|
+
return paymentHooks!;
|
|
92
|
+
},
|
|
93
|
+
onSettleFailure: (h) => {
|
|
94
|
+
this.payment!.onSettleFailure(h);
|
|
95
|
+
return paymentHooks!;
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
72
100
|
const ctx: RegisterContext = {
|
|
73
101
|
route: (method, path, handler, options) => {
|
|
74
102
|
this.route(method, path, handler, options);
|
|
@@ -77,6 +105,7 @@ export class AixyzApp {
|
|
|
77
105
|
if (entry) plugin.registeredRoutes.set(key, entry);
|
|
78
106
|
},
|
|
79
107
|
use: (mw) => this.use(mw),
|
|
108
|
+
paymentHooks,
|
|
80
109
|
};
|
|
81
110
|
await plugin.register?.(ctx);
|
|
82
111
|
return this;
|
|
@@ -87,14 +116,21 @@ export class AixyzApp {
|
|
|
87
116
|
return `${method} ${path}`;
|
|
88
117
|
}
|
|
89
118
|
|
|
90
|
-
/** Register a route with an optional x402 payment requirement. */
|
|
91
|
-
route(method: HttpMethod, path: string, handler: RouteHandler, options?:
|
|
119
|
+
/** Register a route with an optional x402 payment requirement. Free accepts are filtered out. */
|
|
120
|
+
route(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
92
121
|
const key = this.getRouteKey(method, path);
|
|
122
|
+
if (options?.payment) {
|
|
123
|
+
const result = AcceptsScheme.safeParse(options.payment);
|
|
124
|
+
if (!result.success) {
|
|
125
|
+
throw new Error(`Invalid accepts config for route "${method} ${path}": ${result.error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const payment = options?.payment && isAcceptsPaid(options.payment) ? options.payment : undefined;
|
|
93
129
|
this.routes.set(key, {
|
|
94
130
|
method,
|
|
95
131
|
path,
|
|
96
132
|
handler,
|
|
97
|
-
payment
|
|
133
|
+
payment,
|
|
98
134
|
});
|
|
99
135
|
}
|
|
100
136
|
|
package/app/payment/payment.ts
CHANGED
|
@@ -8,10 +8,56 @@ import {
|
|
|
8
8
|
type HTTPResponseInstructions,
|
|
9
9
|
} from "@x402/core/http";
|
|
10
10
|
import { ExactEvmScheme } from "@x402/evm/exact/server";
|
|
11
|
-
import type { AcceptsX402 } from "../../accepts";
|
|
12
|
-
import {
|
|
11
|
+
import type { AcceptsX402, AcceptsX402Multi } from "../../accepts";
|
|
12
|
+
import { normalizeAcceptsX402 } from "../../accepts";
|
|
13
|
+
import { Network, PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse } from "@x402/core/types";
|
|
13
14
|
import { AixyzConfig } from "@aixyz/config";
|
|
14
15
|
|
|
16
|
+
// ── x402 Lifecycle Hook Types ──────────────────────────────────────────
|
|
17
|
+
// These mirror x402ResourceServer's hook signatures but are defined here
|
|
18
|
+
// because x402/core does not export them publicly.
|
|
19
|
+
|
|
20
|
+
export interface VerifyContext {
|
|
21
|
+
paymentPayload: PaymentPayload;
|
|
22
|
+
requirements: PaymentRequirements;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface VerifyResultContext extends VerifyContext {
|
|
26
|
+
result: VerifyResponse;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VerifyFailureContext extends VerifyContext {
|
|
30
|
+
error: Error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SettleContext {
|
|
34
|
+
paymentPayload: PaymentPayload;
|
|
35
|
+
requirements: PaymentRequirements;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SettleResultContext extends SettleContext {
|
|
39
|
+
result: SettleResponse;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SettleFailureContext extends SettleContext {
|
|
43
|
+
error: Error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type BeforeVerifyHook = (
|
|
47
|
+
context: VerifyContext,
|
|
48
|
+
) => Promise<void | { abort: true; reason: string; message?: string }>;
|
|
49
|
+
export type AfterVerifyHook = (context: VerifyResultContext) => Promise<void>;
|
|
50
|
+
export type OnVerifyFailureHook = (
|
|
51
|
+
context: VerifyFailureContext,
|
|
52
|
+
) => Promise<void | { recovered: true; result: VerifyResponse }>;
|
|
53
|
+
export type BeforeSettleHook = (
|
|
54
|
+
context: SettleContext,
|
|
55
|
+
) => Promise<void | { abort: true; reason: string; message?: string }>;
|
|
56
|
+
export type AfterSettleHook = (context: SettleResultContext) => Promise<void>;
|
|
57
|
+
export type OnSettleFailureHook = (
|
|
58
|
+
context: SettleFailureContext,
|
|
59
|
+
) => Promise<void | { recovered: true; result: SettleResponse }>;
|
|
60
|
+
|
|
15
61
|
/**
|
|
16
62
|
* Converts a web-standard Request into the x402 HTTPAdapter interface.
|
|
17
63
|
*/
|
|
@@ -44,10 +90,12 @@ function toResponse(instructions: HTTPResponseInstructions): Response {
|
|
|
44
90
|
return new Response(body, { status: instructions.status, headers });
|
|
45
91
|
}
|
|
46
92
|
|
|
47
|
-
interface PaymentContext {
|
|
93
|
+
export interface PaymentContext {
|
|
48
94
|
paymentPayload: PaymentPayload;
|
|
49
95
|
paymentRequirements: PaymentRequirements;
|
|
50
96
|
declaredExtensions?: Record<string, unknown>;
|
|
97
|
+
/** The payer's address, captured from x402 verification. */
|
|
98
|
+
payer?: string;
|
|
51
99
|
}
|
|
52
100
|
|
|
53
101
|
/**
|
|
@@ -59,10 +107,22 @@ export class PaymentGateway {
|
|
|
59
107
|
private readonly config: AixyzConfig;
|
|
60
108
|
private readonly pendingRoutes = new Map<string, RouteConfig>();
|
|
61
109
|
private readonly verifiedPayments = new WeakMap<Request, PaymentContext>();
|
|
110
|
+
/**
|
|
111
|
+
* Temporary map to capture payer address from the afterVerify hook.
|
|
112
|
+
* Keyed by PaymentPayload reference (same object flows through the verify pipeline).
|
|
113
|
+
*/
|
|
114
|
+
private readonly pendingPayers = new WeakMap<PaymentPayload, string>();
|
|
62
115
|
|
|
63
116
|
constructor(facilitators: FacilitatorClient | FacilitatorClient[], config: AixyzConfig) {
|
|
64
117
|
this.resourceServer = new x402ResourceServer(facilitators);
|
|
65
118
|
this.config = config;
|
|
119
|
+
|
|
120
|
+
// Capture payer address from verification so it can be exposed to handlers.
|
|
121
|
+
this.onAfterVerify(async (context) => {
|
|
122
|
+
if (context.result.payer) {
|
|
123
|
+
this.pendingPayers.set(context.paymentPayload, context.result.payer);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
66
126
|
}
|
|
67
127
|
|
|
68
128
|
/** Register an EVM payment scheme for the given network (e.g. Base mainnet). */
|
|
@@ -78,29 +138,49 @@ export class PaymentGateway {
|
|
|
78
138
|
/**
|
|
79
139
|
* Add a payment-gated route. Must be called before initialize().
|
|
80
140
|
*/
|
|
81
|
-
addRoute(method: string, path: string, accepts: AcceptsX402): void {
|
|
141
|
+
addRoute(method: string, path: string, accepts: AcceptsX402 | AcceptsX402Multi): void {
|
|
82
142
|
const pattern = this.getRouteKey(method, path);
|
|
143
|
+
const items = normalizeAcceptsX402(accepts);
|
|
83
144
|
this.pendingRoutes.set(pattern, {
|
|
84
|
-
accepts: {
|
|
85
|
-
scheme:
|
|
86
|
-
payTo:
|
|
87
|
-
price:
|
|
88
|
-
network: (
|
|
89
|
-
},
|
|
145
|
+
accepts: items.map((a) => ({
|
|
146
|
+
scheme: a.scheme,
|
|
147
|
+
payTo: a.payTo ?? this.config.x402.payTo,
|
|
148
|
+
price: a.price,
|
|
149
|
+
network: (a.network as Network) ?? (this.config.x402.network as Network),
|
|
150
|
+
})),
|
|
90
151
|
});
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
/**
|
|
94
|
-
* Initialize the payment gateway.
|
|
155
|
+
* Initialize the payment gateway. Registers all required network schemes
|
|
156
|
+
* from pending routes, then builds the x402HTTPResourceServer.
|
|
95
157
|
* Must be called after all routes are added.
|
|
96
158
|
*/
|
|
97
159
|
async initialize(): Promise<void> {
|
|
160
|
+
this.registerNetworksFromRoutes();
|
|
98
161
|
const routes: RoutesConfig =
|
|
99
162
|
this.pendingRoutes.size > 0 ? Object.fromEntries(this.pendingRoutes) : { "* /*": { accepts: [] } };
|
|
100
163
|
this.httpServer = new x402HTTPResourceServer(this.resourceServer, routes);
|
|
101
164
|
await this.httpServer.initialize();
|
|
102
165
|
}
|
|
103
166
|
|
|
167
|
+
private registerNetworksFromRoutes(): void {
|
|
168
|
+
const networks = new Set<Network>();
|
|
169
|
+
const defaultNetwork = (this.config.x402.network as Network) ?? ("eip155:8453" as Network);
|
|
170
|
+
networks.add(defaultNetwork);
|
|
171
|
+
|
|
172
|
+
for (const [, route] of this.pendingRoutes) {
|
|
173
|
+
const accepts = Array.isArray(route.accepts) ? route.accepts : [route.accepts];
|
|
174
|
+
for (const opt of accepts) {
|
|
175
|
+
if (opt.network) networks.add(opt.network as Network);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const network of networks) {
|
|
180
|
+
this.register(network);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
104
184
|
/**
|
|
105
185
|
* Verify payment for a request. Returns a 402 Response if payment is required/invalid,
|
|
106
186
|
* or null if the request is authorized to proceed.
|
|
@@ -124,13 +204,16 @@ export class PaymentGateway {
|
|
|
124
204
|
case "no-payment-required":
|
|
125
205
|
return null;
|
|
126
206
|
|
|
127
|
-
case "payment-verified":
|
|
207
|
+
case "payment-verified": {
|
|
208
|
+
const payer = this.pendingPayers.get(result.paymentPayload);
|
|
128
209
|
this.verifiedPayments.set(request, {
|
|
129
210
|
paymentPayload: result.paymentPayload,
|
|
130
211
|
paymentRequirements: result.paymentRequirements,
|
|
131
212
|
declaredExtensions: result.declaredExtensions,
|
|
213
|
+
payer,
|
|
132
214
|
});
|
|
133
215
|
return null;
|
|
216
|
+
}
|
|
134
217
|
|
|
135
218
|
case "payment-error":
|
|
136
219
|
return toResponse(result.response);
|
|
@@ -154,4 +237,58 @@ export class PaymentGateway {
|
|
|
154
237
|
|
|
155
238
|
return result;
|
|
156
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get the payer's address for a verified payment request.
|
|
243
|
+
* Available after `verify()` succeeds and before `settle()` is called.
|
|
244
|
+
*/
|
|
245
|
+
getPayer(request: Request): string | undefined {
|
|
246
|
+
return this.verifiedPayments.get(request)?.payer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the full payment context for a verified payment request.
|
|
251
|
+
* Available after `verify()` succeeds and before `settle()` is called.
|
|
252
|
+
*/
|
|
253
|
+
getPaymentContext(request: Request): Readonly<PaymentContext> | undefined {
|
|
254
|
+
return this.verifiedPayments.get(request);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── x402 Lifecycle Hooks ───────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/** Register a hook to run before payment verification. Return `{ abort: true, reason }` to reject. */
|
|
260
|
+
onBeforeVerify(hook: BeforeVerifyHook): this {
|
|
261
|
+
this.resourceServer.onBeforeVerify(hook);
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Register a hook to run after successful payment verification. */
|
|
266
|
+
onAfterVerify(hook: AfterVerifyHook): this {
|
|
267
|
+
this.resourceServer.onAfterVerify(hook);
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Register a hook to run when payment verification fails. Return `{ recovered: true, result }` to recover. */
|
|
272
|
+
onVerifyFailure(hook: OnVerifyFailureHook): this {
|
|
273
|
+
this.resourceServer.onVerifyFailure(hook);
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Register a hook to run before payment settlement. Return `{ abort: true, reason }` to reject. */
|
|
278
|
+
onBeforeSettle(hook: BeforeSettleHook): this {
|
|
279
|
+
this.resourceServer.onBeforeSettle(hook);
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Register a hook to run after successful payment settlement. */
|
|
284
|
+
onAfterSettle(hook: AfterSettleHook): this {
|
|
285
|
+
this.resourceServer.onAfterSettle(hook);
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Register a hook to run when payment settlement fails. Return `{ recovered: true, result }` to recover. */
|
|
290
|
+
onSettleFailure(hook: OnSettleFailureHook): this {
|
|
291
|
+
this.resourceServer.onSettleFailure(hook);
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
157
294
|
}
|
package/app/plugin.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
import type { HttpMethod, RouteHandler, RouteEntry, Middleware } from "./types";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
1
|
+
import type { HttpMethod, RouteHandler, RouteEntry, RouteOptions, Middleware } from "./types";
|
|
2
|
+
import type {
|
|
3
|
+
PaymentGateway,
|
|
4
|
+
BeforeVerifyHook,
|
|
5
|
+
AfterVerifyHook,
|
|
6
|
+
OnVerifyFailureHook,
|
|
7
|
+
BeforeSettleHook,
|
|
8
|
+
AfterSettleHook,
|
|
9
|
+
OnSettleFailureHook,
|
|
10
|
+
} from "./payment/payment";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scoped interface for registering x402 payment lifecycle hooks.
|
|
14
|
+
* Exposes only hook registration — not the full PaymentGateway.
|
|
15
|
+
*/
|
|
16
|
+
export interface PaymentHookContext {
|
|
17
|
+
onBeforeVerify(hook: BeforeVerifyHook): PaymentHookContext;
|
|
18
|
+
onAfterVerify(hook: AfterVerifyHook): PaymentHookContext;
|
|
19
|
+
onVerifyFailure(hook: OnVerifyFailureHook): PaymentHookContext;
|
|
20
|
+
onBeforeSettle(hook: BeforeSettleHook): PaymentHookContext;
|
|
21
|
+
onAfterSettle(hook: AfterSettleHook): PaymentHookContext;
|
|
22
|
+
onSettleFailure(hook: OnSettleFailureHook): PaymentHookContext;
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
/**
|
|
6
26
|
* Scoped context passed to {@link BasePlugin.register}.
|
|
@@ -14,9 +34,11 @@ import type { PaymentGateway } from "./payment/payment";
|
|
|
14
34
|
*/
|
|
15
35
|
export interface RegisterContext {
|
|
16
36
|
/** Register a route on the application. Automatically tracked in `plugin.registeredRoutes`. */
|
|
17
|
-
route(method: HttpMethod, path: string, handler: RouteHandler, options?:
|
|
37
|
+
route(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
18
38
|
/** Append a middleware to the application's middleware chain. */
|
|
19
39
|
use(middleware: Middleware): void;
|
|
40
|
+
/** Register hooks on the x402 payment lifecycle. Undefined when no facilitators configured. */
|
|
41
|
+
readonly paymentHooks?: PaymentHookContext;
|
|
20
42
|
}
|
|
21
43
|
|
|
22
44
|
/**
|
package/app/plugins/a2a.ts
CHANGED
|
@@ -25,7 +25,8 @@ export type Capabilities = z.infer<typeof CapabilitiesSchema>;
|
|
|
25
25
|
|
|
26
26
|
export interface A2AAgentEntry {
|
|
27
27
|
name?: string;
|
|
28
|
-
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
exports: { default: ToolLoopAgent<never, any>; accepts?: Accepts; capabilities?: Capabilities };
|
|
29
30
|
/** Optional task store override. Defaults to a per-agent InMemoryTaskStore for isolation. */
|
|
30
31
|
taskStore?: TaskStore;
|
|
31
32
|
}
|
|
@@ -250,7 +251,7 @@ export class A2APlugin extends BasePlugin {
|
|
|
250
251
|
return Response.json(result);
|
|
251
252
|
},
|
|
252
253
|
{
|
|
253
|
-
payment: entry.exports.accepts
|
|
254
|
+
payment: entry.exports.accepts,
|
|
254
255
|
},
|
|
255
256
|
);
|
|
256
257
|
}
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { BasePlugin, type RegisterContext, type InitializeContext } from "../../plugin";
|
|
4
4
|
import type { MCPPlugin } from "../mcp";
|
|
5
5
|
import { renderHtml } from "./html";
|
|
6
|
+
import { isAcceptsPaid } from "../../../accepts";
|
|
6
7
|
|
|
7
8
|
export type AixyzConfigRuntime = ReturnType<typeof getAixyzConfigRuntime>;
|
|
8
9
|
|
|
@@ -141,7 +142,7 @@ export class IndexPagePlugin extends BasePlugin {
|
|
|
141
142
|
protocol: "a2a",
|
|
142
143
|
name,
|
|
143
144
|
path: entry.path,
|
|
144
|
-
paid: entry.payment
|
|
145
|
+
paid: !!entry.payment, // only set for paid routes (free accepts filtered in AixyzApp.route())
|
|
145
146
|
});
|
|
146
147
|
}
|
|
147
148
|
}
|
|
@@ -170,7 +171,7 @@ export class IndexPagePlugin extends BasePlugin {
|
|
|
170
171
|
name: tool.name,
|
|
171
172
|
path: "/mcp",
|
|
172
173
|
description: tool.tool.description,
|
|
173
|
-
paid: tool.accepts
|
|
174
|
+
paid: tool.accepts ? isAcceptsPaid(tool.accepts) : false,
|
|
174
175
|
inputSchema,
|
|
175
176
|
});
|
|
176
177
|
}
|
package/app/plugins/mcp.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { type Tool } from "ai";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
5
|
import { createPaymentWrapper } from "@x402/mcp";
|
|
5
6
|
import { BasePlugin, type RegisterContext, type InitializeContext } from "../plugin";
|
|
6
7
|
import type { Accepts } from "../../accepts";
|
|
7
|
-
import { AcceptsScheme } from "../../accepts";
|
|
8
|
+
import { AcceptsScheme, isAcceptsPaid, normalizeAcceptsX402 } from "../../accepts";
|
|
8
9
|
import { getAixyzConfig, getAixyzConfigRuntime } from "../../config";
|
|
9
10
|
import { Network } from "@x402/core/types";
|
|
11
|
+
import { ExactEvmScheme } from "@x402/evm/exact/server";
|
|
12
|
+
import type { SessionPlugin } from "./session/index";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* AsyncLocalStorage to pass the MCP-level payer address from the
|
|
16
|
+
* onAfterVerify hook to the tool handler within the same async context.
|
|
17
|
+
*/
|
|
18
|
+
const mcpPayerStorage = new AsyncLocalStorage<{ payer?: string }>();
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
21
|
* MCP (Model Context Protocol) plugin. Collects tools and exposes them
|
|
@@ -20,6 +29,7 @@ export class MCPPlugin extends BasePlugin {
|
|
|
20
29
|
readonly name = "mcp";
|
|
21
30
|
readonly registeredTools: Array<{ name: string; tool: Tool; accepts?: Accepts }> = [];
|
|
22
31
|
private paymentWrappers = new Map<string, (handler: any) => any>();
|
|
32
|
+
private sessionPlugin?: SessionPlugin;
|
|
23
33
|
|
|
24
34
|
constructor(private tools: Array<{ name: string; exports: { default: Tool; accepts?: Accepts } }>) {
|
|
25
35
|
super();
|
|
@@ -30,22 +40,44 @@ export class MCPPlugin extends BasePlugin {
|
|
|
30
40
|
const mcpServer = new McpServer({ name: config.name, version: config.version }, { capabilities: { tools: {} } });
|
|
31
41
|
|
|
32
42
|
for (const { name, tool } of this.registeredTools) {
|
|
43
|
+
const sessionPlugin = this.sessionPlugin;
|
|
33
44
|
const handler = async (args: Record<string, unknown>) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const execute = async () => {
|
|
46
|
+
try {
|
|
47
|
+
const result = await tool.execute!(args, { toolCallId: name, messages: [] });
|
|
48
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
49
|
+
return { content: [{ type: "text" as const, text }] };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[mcp] Tool "${name}" failed:`, error);
|
|
52
|
+
const text = error instanceof Error ? error.message : "Unknown error";
|
|
53
|
+
return { content: [{ type: "text" as const, text: `Error: ${text}` }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// If a payer was captured from MCP-level payment, run the tool
|
|
58
|
+
// within a session context so getSession() works.
|
|
59
|
+
const payer = mcpPayerStorage.getStore()?.payer;
|
|
60
|
+
if (payer && sessionPlugin) {
|
|
61
|
+
return sessionPlugin.runWithPayer(payer, execute);
|
|
41
62
|
}
|
|
63
|
+
return execute();
|
|
42
64
|
};
|
|
43
65
|
|
|
44
66
|
const wrapper = this.paymentWrappers.get(name);
|
|
67
|
+
let registeredHandler: (args: any, extra?: any) => any;
|
|
68
|
+
if (wrapper) {
|
|
69
|
+
const wrapped = wrapper(handler);
|
|
70
|
+
// Wrap the payment wrapper call in mcpPayerStorage.run() so the
|
|
71
|
+
// onAfterVerify hook and handler share the same async context.
|
|
72
|
+
registeredHandler = (args: any, extra: any) =>
|
|
73
|
+
mcpPayerStorage.run({ payer: undefined }, () => wrapped(args, extra));
|
|
74
|
+
} else {
|
|
75
|
+
registeredHandler = handler;
|
|
76
|
+
}
|
|
45
77
|
mcpServer.registerTool(
|
|
46
78
|
name,
|
|
47
79
|
{ description: tool.description, inputSchema: tool.inputSchema as any },
|
|
48
|
-
|
|
80
|
+
registeredHandler,
|
|
49
81
|
);
|
|
50
82
|
}
|
|
51
83
|
|
|
@@ -79,25 +111,60 @@ export class MCPPlugin extends BasePlugin {
|
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
async initialize(ctx: InitializeContext): Promise<void> {
|
|
114
|
+
this.sessionPlugin = ctx.getPlugin<SessionPlugin>("session");
|
|
115
|
+
|
|
82
116
|
if (!ctx.payment) return;
|
|
83
117
|
|
|
84
118
|
const config = getAixyzConfig();
|
|
85
119
|
const resourceServer = ctx.payment.resourceServer;
|
|
120
|
+
const defaultNetwork = (config.x402.network as Network) ?? ("eip155:8453" as Network);
|
|
121
|
+
|
|
122
|
+
// MCP payment operates via @x402/mcp wrappers independently of the HTTP payment
|
|
123
|
+
// middleware (PaymentGateway), so network schemes must be registered here separately.
|
|
124
|
+
// The default network is already registered by PaymentGateway.initialize() on the
|
|
125
|
+
// shared resourceServer — seed the set so we skip re-registering it.
|
|
126
|
+
const registeredNetworks = new Set<string>([defaultNetwork]);
|
|
127
|
+
for (const { accepts } of this.registeredTools) {
|
|
128
|
+
if (!accepts || !isAcceptsPaid(accepts)) continue;
|
|
129
|
+
for (const a of normalizeAcceptsX402(accepts)) {
|
|
130
|
+
const network = a.network ?? defaultNetwork;
|
|
131
|
+
if (!registeredNetworks.has(network)) {
|
|
132
|
+
registeredNetworks.add(network);
|
|
133
|
+
resourceServer.register(network as Network, new ExactEvmScheme());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
86
137
|
|
|
87
|
-
|
|
88
|
-
|
|
138
|
+
// Capture payer from MCP-level payment verification for session integration.
|
|
139
|
+
// This hook is global (fires for all verifications, including HTTP-level), but
|
|
140
|
+
// only writes when mcpPayerStorage has an active context — which only happens
|
|
141
|
+
// inside MCP tool calls wrapped by mcpPayerStorage.run() below.
|
|
142
|
+
ctx.payment.onAfterVerify(async (context) => {
|
|
143
|
+
const store = mcpPayerStorage.getStore();
|
|
144
|
+
if (store && context.result.payer) {
|
|
145
|
+
store.payer = context.result.payer;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
89
148
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
for (const { name, accepts } of this.registeredTools) {
|
|
150
|
+
if (!accepts || !isAcceptsPaid(accepts)) continue;
|
|
151
|
+
|
|
152
|
+
const items = normalizeAcceptsX402(accepts);
|
|
153
|
+
const allReqs: Awaited<ReturnType<typeof resourceServer.buildPaymentRequirements>> = [];
|
|
154
|
+
for (const a of items) {
|
|
155
|
+
const reqs = await resourceServer.buildPaymentRequirements({
|
|
156
|
+
scheme: a.scheme,
|
|
157
|
+
payTo: a.payTo ?? config.x402.payTo,
|
|
158
|
+
price: a.price,
|
|
159
|
+
network: (a.network as Network) ?? defaultNetwork,
|
|
160
|
+
});
|
|
161
|
+
allReqs.push(...reqs);
|
|
162
|
+
}
|
|
96
163
|
|
|
97
164
|
this.paymentWrappers.set(
|
|
98
165
|
name,
|
|
99
166
|
createPaymentWrapper(resourceServer, {
|
|
100
|
-
accepts:
|
|
167
|
+
accepts: allReqs,
|
|
101
168
|
resource: { url: `mcp://tool/${name}` },
|
|
102
169
|
}),
|
|
103
170
|
);
|