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 CHANGED
@@ -1,32 +1,47 @@
1
1
  import { z } from "zod";
2
2
  import { FacilitatorClient, HTTPFacilitatorClient } from "@x402/core/server";
3
3
 
4
- export type Accepts = AcceptsX402 | AcceptsFree;
5
-
6
- export type AcceptsX402 = {
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?: string;
11
- payTo?: string;
12
- };
13
-
14
- export type AcceptsFree = {
15
- scheme: "free";
16
- };
17
-
18
- export const AcceptsScheme = z.discriminatedUnion("scheme", [
19
- z.object({
20
- scheme: z.literal("exact"),
21
- price: z.string(),
22
- network: z.string().optional(),
23
- payTo: z.string().optional(),
24
- }),
25
- z.object({
26
- scheme: z.literal("free"),
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 type { AcceptsX402 } from "../accepts";
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?: { payment?: AcceptsX402 }): void {
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: options?.payment,
133
+ payment,
98
134
  });
99
135
  }
100
136
 
@@ -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 { Network, PaymentPayload, PaymentRequirements } from "@x402/core/types";
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: accepts.scheme,
86
- payTo: accepts.payTo ?? this.config.x402.payTo,
87
- price: accepts.price,
88
- network: (accepts.network as Network) ?? (this.config.x402.network as 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. Builds the x402HTTPResourceServer from registered routes.
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 { AcceptsX402 } from "../accepts";
3
- import type { PaymentGateway } from "./payment/payment";
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?: { payment?: AcceptsX402 }): void;
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
  /**
@@ -25,7 +25,8 @@ export type Capabilities = z.infer<typeof CapabilitiesSchema>;
25
25
 
26
26
  export interface A2AAgentEntry {
27
27
  name?: string;
28
- exports: { default: ToolLoopAgent; accepts?: Accepts; capabilities?: Capabilities };
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.scheme === "exact" ? entry.exports.accepts : undefined,
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?.scheme === "exact",
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?.scheme === "exact",
174
+ paid: tool.accepts ? isAcceptsPaid(tool.accepts) : false,
174
175
  inputSchema,
175
176
  });
176
177
  }
@@ -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
- try {
35
- const result = await tool.execute!(args, { toolCallId: name, messages: [] });
36
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
37
- return { content: [{ type: "text" as const, text }] };
38
- } catch (error) {
39
- const text = error instanceof Error ? error.message : "Unknown error";
40
- return { content: [{ type: "text" as const, text: `Error: ${text}` }], isError: true };
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
- wrapper ? wrapper(handler) : handler,
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
- for (const { name, accepts } of this.registeredTools) {
88
- if (accepts?.scheme !== "exact") continue;
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
- const reqs = await resourceServer.buildPaymentRequirements({
91
- scheme: accepts.scheme,
92
- payTo: accepts.payTo ?? config.x402.payTo,
93
- price: accepts.price,
94
- network: (accepts.network as Network) ?? (config.x402.network as Network),
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: reqs,
167
+ accepts: allReqs,
101
168
  resource: { url: `mcp://tool/${name}` },
102
169
  }),
103
170
  );