1- import { afterAll , beforeAll , describe , expect , test , spyOn } from "bun:test" ;
1+ import { afterAll , beforeAll , describe , expect , setDefaultTimeout , test , spyOn } from "bun:test" ;
22import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
33import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" ;
4+ import { generatePrivateKey } from "viem/accounts" ;
45import { listMcpTools , callMcpTool } from "./mcp" ;
56import { createMcpPaymentClient , DryRunPaymentRequired , USER_AGENT } from "./client" ;
67import { EvmPrivateKeyWallet } from "./wallets/evm-private-key" ;
@@ -15,6 +16,8 @@ import {
1516import { accounts } from "x402-fl/testcontainers" ;
1617import pkg from "./package.json" with { type : "json" } ;
1718
19+ setDefaultTimeout ( 30_000 ) ;
20+
1821let fixture : X402FacilitatorLocal ;
1922
2023beforeAll ( async ( ) => {
@@ -36,7 +39,7 @@ async function createMcpClient(): Promise<Client> {
3639 return client ;
3740}
3841
39- describe ( "mcp free (sdk) " , ( ) => {
42+ describe ( "mcp free" , ( ) => {
4043 test ( "calls echo tool and returns text content" , async ( ) => {
4144 const client = await createMcpClient ( ) ;
4245 try {
@@ -54,7 +57,7 @@ describe("mcp free (sdk)", () => {
5457 } ) ;
5558} ) ;
5659
57- describe ( "mcp x402 payment (sdk) " , ( ) => {
60+ describe ( "mcp x402 payment" , ( ) => {
5861 test ( "paid tool call succeeds with funded wallet and debits sender exactly $0.001" , async ( ) => {
5962 const wallet = new EvmPrivateKeyWallet ( TEST_PRIVATE_KEY , fixture . container . getRpcUrl ( ) ) ;
6063 const client = await createMcpClient ( ) ;
@@ -89,7 +92,7 @@ describe("mcp x402 payment (sdk)", () => {
8992 } ) ;
9093} ) ;
9194
92- describe ( "listMcpTools (high-level) " , ( ) => {
95+ describe ( "listMcpTools" , ( ) => {
9396 test ( "returns array including echo tool" , async ( ) => {
9497 const tools = await listMcpTools ( mcpUrl ( ) ) ;
9598 expect ( Array . isArray ( tools ) ) . toBe ( true ) ;
@@ -109,7 +112,7 @@ describe("listMcpTools (high-level)", () => {
109112 } ) ;
110113} ) ;
111114
112- describe ( "callMcpTool (high-level) " , ( ) => {
115+ describe ( "callMcpTool" , ( ) => {
113116 test ( "free tool call succeeds in dry-run mode" , async ( ) => {
114117 const result = await callMcpTool ( mcpUrl ( ) , "echo" , { message : "hello high-level" } ) ;
115118 const content = result . content as Array < { type : string ; text : string } > ;
@@ -152,4 +155,91 @@ describe("callMcpTool (high-level)", () => {
152155 expect ( err . message ) . toContain ( "USDC" ) ;
153156 }
154157 } ) ;
158+
159+ test ( "custom fetchImpl is used for paid tool call" , async ( ) => {
160+ const wallet = new EvmPrivateKeyWallet ( TEST_PRIVATE_KEY , fixture . container . getRpcUrl ( ) ) ;
161+ const spy = spyOn ( globalThis , "fetch" ) ;
162+ try {
163+ // @ts -expect-error — Bun's typeof fetch includes preconnect namespace (oven-sh/bun#23741)
164+ const customFetch : typeof fetch = ( input , init ) => {
165+ const headers = new Headers ( init ?. headers ) ;
166+ headers . set ( "X-Custom-Header" , "pay-test" ) ;
167+ return globalThis . fetch ( input , { ...init , headers } ) ;
168+ } ;
169+
170+ const result = await callMcpTool (
171+ mcpUrl ( ) ,
172+ "paid-echo-tool" ,
173+ { message : "hello custom fetch pay" } ,
174+ {
175+ transaction : PayTransaction ( wallet ) ,
176+ fetchImpl : customFetch ,
177+ } ,
178+ ) ;
179+ const content = result . content as Array < { type : string ; text : string } > ;
180+ expect ( content [ 0 ] . text ) . toStrictEqual ( "hello custom fetch pay" ) ;
181+
182+ // Verify the custom header was sent on at least one underlying fetch call
183+ const hasCustomHeader = spy . mock . calls . some ( ( call ) => {
184+ const headers = new Headers ( call [ 1 ] ?. headers ) ;
185+ return headers . get ( "X-Custom-Header" ) === "pay-test" ;
186+ } ) ;
187+ expect ( hasCustomHeader ) . toBe ( true ) ;
188+ } finally {
189+ spy . mockRestore ( ) ;
190+ }
191+ } ) ;
192+
193+ test ( "custom fetchImpl is used for dry-run tool call" , async ( ) => {
194+ const spy = spyOn ( globalThis , "fetch" ) ;
195+ try {
196+ // @ts -expect-error — Bun's typeof fetch includes preconnect namespace (oven-sh/bun#23741)
197+ const customFetch : typeof fetch = ( input , init ) => {
198+ const headers = new Headers ( init ?. headers ) ;
199+ headers . set ( "X-Custom-Header" , "dryrun-test" ) ;
200+ return globalThis . fetch ( input , { ...init , headers } ) ;
201+ } ;
202+
203+ const result = await callMcpTool (
204+ mcpUrl ( ) ,
205+ "echo" ,
206+ { message : "hello custom fetch dryrun" } ,
207+ { fetchImpl : customFetch } ,
208+ ) ;
209+ const content = result . content as Array < { type : string ; text : string } > ;
210+ expect ( content [ 0 ] . text ) . toStrictEqual ( "hello custom fetch dryrun" ) ;
211+
212+ const hasCustomHeader = spy . mock . calls . some ( ( call ) => {
213+ const headers = new Headers ( call [ 1 ] ?. headers ) ;
214+ return headers . get ( "X-Custom-Header" ) === "dryrun-test" ;
215+ } ) ;
216+ expect ( hasCustomHeader ) . toBe ( true ) ;
217+ } finally {
218+ spy . mockRestore ( ) ;
219+ }
220+ } ) ;
221+
222+ test ( "paid tool with unfunded wallet returns isError with insufficient_funds" , async ( ) => {
223+ const unfundedKey = generatePrivateKey ( ) ;
224+ const unfundedWallet = new EvmPrivateKeyWallet ( unfundedKey , fixture . container . getRpcUrl ( ) ) ;
225+
226+ const result = await callMcpTool (
227+ mcpUrl ( ) ,
228+ "paid-echo-tool" ,
229+ { message : "should fail — no funds" } ,
230+ { transaction : PayTransaction ( unfundedWallet ) } ,
231+ ) ;
232+
233+ expect ( result . isError ) . toStrictEqual ( true ) ;
234+
235+ const content = result . content as Array < { type : string ; text ?: string } > ;
236+ expect ( content . length ) . toBeGreaterThan ( 0 ) ;
237+ expect ( content [ 0 ] . type ) . toStrictEqual ( "text" ) ;
238+
239+ const parsed = JSON . parse ( content [ 0 ] . text ! ) ;
240+ expect ( parsed . x402Version ) . toBeDefined ( ) ;
241+ expect ( parsed . accepts ) . toBeDefined ( ) ;
242+ expect ( Array . isArray ( parsed . accepts ) ) . toBe ( true ) ;
243+ expect ( parsed . error ) . toContain ( "insufficient_funds" ) ;
244+ } ) ;
155245} ) ;
0 commit comments