mppx 0.7.0 → 0.8.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.
Files changed (278) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +20 -11
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +18 -6
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Mcp.d.ts +3 -0
  7. package/dist/Mcp.d.ts.map +1 -1
  8. package/dist/Mcp.js +2 -0
  9. package/dist/Mcp.js.map +1 -1
  10. package/dist/PaymentRequest.d.ts +10 -10
  11. package/dist/PaymentRequest.js +8 -8
  12. package/dist/client/Mppx.js +2 -2
  13. package/dist/client/Mppx.js.map +1 -1
  14. package/dist/client/Transport.d.ts +11 -16
  15. package/dist/client/Transport.d.ts.map +1 -1
  16. package/dist/client/Transport.js +55 -75
  17. package/dist/client/Transport.js.map +1 -1
  18. package/dist/client/index.d.ts +3 -0
  19. package/dist/client/index.d.ts.map +1 -1
  20. package/dist/client/index.js +1 -0
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/internal/Fetch.d.ts.map +1 -1
  23. package/dist/client/internal/Fetch.js +46 -7
  24. package/dist/client/internal/Fetch.js.map +1 -1
  25. package/dist/client/internal/protocols/Mcp.d.ts +7 -0
  26. package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
  27. package/dist/client/internal/protocols/Mcp.js +159 -0
  28. package/dist/client/internal/protocols/Mcp.js.map +1 -0
  29. package/dist/client/internal/protocols/Mpp.d.ts +4 -0
  30. package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
  31. package/dist/client/internal/protocols/Mpp.js +18 -0
  32. package/dist/client/internal/protocols/Mpp.js.map +1 -0
  33. package/dist/client/internal/protocols/Protocol.d.ts +10 -0
  34. package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
  35. package/dist/client/internal/protocols/Protocol.js +2 -0
  36. package/dist/client/internal/protocols/Protocol.js.map +1 -0
  37. package/dist/client/internal/protocols/Shared.d.ts +5 -0
  38. package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
  39. package/dist/client/internal/protocols/Shared.js +20 -0
  40. package/dist/client/internal/protocols/Shared.js.map +1 -0
  41. package/dist/client/internal/protocols/X402.d.ts +8 -0
  42. package/dist/client/internal/protocols/X402.d.ts.map +1 -0
  43. package/dist/client/internal/protocols/X402.js +39 -0
  44. package/dist/client/internal/protocols/X402.js.map +1 -0
  45. package/dist/evm/client/index.d.ts +1 -0
  46. package/dist/evm/client/index.d.ts.map +1 -1
  47. package/dist/evm/client/index.js +1 -0
  48. package/dist/evm/client/index.js.map +1 -1
  49. package/dist/evm/index.d.ts +2 -0
  50. package/dist/evm/index.d.ts.map +1 -1
  51. package/dist/evm/index.js +2 -0
  52. package/dist/evm/index.js.map +1 -1
  53. package/dist/evm/server/index.d.ts +1 -0
  54. package/dist/evm/server/index.d.ts.map +1 -1
  55. package/dist/evm/server/index.js +1 -0
  56. package/dist/evm/server/index.js.map +1 -1
  57. package/dist/mcp/client/McpClient.d.ts +101 -0
  58. package/dist/mcp/client/McpClient.d.ts.map +1 -0
  59. package/dist/mcp/client/McpClient.js +162 -0
  60. package/dist/mcp/client/McpClient.js.map +1 -0
  61. package/dist/mcp/client/index.d.ts.map +1 -0
  62. package/dist/mcp/client/index.js.map +1 -0
  63. package/dist/mcp/server/Transport.d.ts.map +1 -0
  64. package/dist/mcp/server/Transport.js.map +1 -0
  65. package/dist/mcp/server/index.d.ts.map +1 -0
  66. package/dist/mcp/server/index.js.map +1 -0
  67. package/dist/server/Mppx.d.ts +1 -1
  68. package/dist/server/Mppx.d.ts.map +1 -1
  69. package/dist/server/Mppx.js +9 -0
  70. package/dist/server/Mppx.js.map +1 -1
  71. package/dist/server/Transport.d.ts +1 -1
  72. package/dist/server/Transport.d.ts.map +1 -1
  73. package/dist/server/Transport.js +1 -1
  74. package/dist/server/Transport.js.map +1 -1
  75. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  76. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  77. package/dist/stripe/server/internal/html.gen.js +1 -1
  78. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  79. package/dist/tempo/Proof.d.ts +85 -1
  80. package/dist/tempo/Proof.d.ts.map +1 -1
  81. package/dist/tempo/Proof.js +35 -0
  82. package/dist/tempo/Proof.js.map +1 -1
  83. package/dist/tempo/client/Charge.d.ts +13 -1
  84. package/dist/tempo/client/Charge.d.ts.map +1 -1
  85. package/dist/tempo/client/Charge.js +38 -25
  86. package/dist/tempo/client/Charge.js.map +1 -1
  87. package/dist/tempo/client/Methods.d.ts +5 -3
  88. package/dist/tempo/client/Methods.d.ts.map +1 -1
  89. package/dist/tempo/client/Methods.js +4 -2
  90. package/dist/tempo/client/Methods.js.map +1 -1
  91. package/dist/tempo/client/ResolveAccount.d.ts +40 -0
  92. package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
  93. package/dist/tempo/client/ResolveAccount.js +2 -0
  94. package/dist/tempo/client/ResolveAccount.js.map +1 -0
  95. package/dist/tempo/internal/fee-payer.d.ts +9 -1
  96. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  97. package/dist/tempo/internal/fee-payer.js +35 -6
  98. package/dist/tempo/internal/fee-payer.js.map +1 -1
  99. package/dist/tempo/internal/proof.d.ts +71 -5
  100. package/dist/tempo/internal/proof.d.ts.map +1 -1
  101. package/dist/tempo/internal/proof.js +42 -6
  102. package/dist/tempo/internal/proof.js.map +1 -1
  103. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
  104. package/dist/tempo/legacy/client/SessionManager.js +10 -3
  105. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  106. package/dist/tempo/server/Charge.d.ts.map +1 -1
  107. package/dist/tempo/server/Charge.js +42 -18
  108. package/dist/tempo/server/Charge.js.map +1 -1
  109. package/dist/tempo/server/Methods.d.ts +4 -2
  110. package/dist/tempo/server/Methods.d.ts.map +1 -1
  111. package/dist/tempo/server/Methods.js +4 -2
  112. package/dist/tempo/server/Methods.js.map +1 -1
  113. package/dist/tempo/server/Subscription.d.ts +10 -0
  114. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  115. package/dist/tempo/server/Subscription.js +135 -23
  116. package/dist/tempo/server/Subscription.js.map +1 -1
  117. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  118. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  119. package/dist/tempo/server/internal/html.gen.js +1 -1
  120. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  121. package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
  122. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  123. package/dist/tempo/session/client/ChannelOps.js +7 -10
  124. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  125. package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
  126. package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
  127. package/dist/tempo/session/client/ChannelStore.js +63 -0
  128. package/dist/tempo/session/client/ChannelStore.js.map +1 -0
  129. package/dist/tempo/session/client/CredentialState.d.ts +7 -24
  130. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  131. package/dist/tempo/session/client/CredentialState.js +51 -49
  132. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  133. package/dist/tempo/session/client/Session.d.ts +8 -2
  134. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  135. package/dist/tempo/session/client/Session.js +22 -8
  136. package/dist/tempo/session/client/Session.js.map +1 -1
  137. package/dist/tempo/session/client/SessionManager.d.ts +4 -40
  138. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  139. package/dist/tempo/session/client/SessionManager.js +124 -174
  140. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  141. package/dist/tempo/session/client/index.d.ts +3 -4
  142. package/dist/tempo/session/client/index.d.ts.map +1 -1
  143. package/dist/tempo/session/client/index.js +1 -0
  144. package/dist/tempo/session/client/index.js.map +1 -1
  145. package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
  146. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  147. package/dist/tempo/session/precompile/Voucher.js +24 -25
  148. package/dist/tempo/session/precompile/Voucher.js.map +1 -1
  149. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  150. package/dist/tempo/session/server/Settlement.js +4 -2
  151. package/dist/tempo/session/server/Settlement.js.map +1 -1
  152. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  153. package/dist/tempo/session/server/Sse.js.map +1 -1
  154. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  155. package/dist/tempo/session/server/Ws.js.map +1 -1
  156. package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
  157. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  158. package/dist/tempo/subscription/Store.d.ts +2 -0
  159. package/dist/tempo/subscription/Store.d.ts.map +1 -1
  160. package/dist/tempo/subscription/Store.js +16 -1
  161. package/dist/tempo/subscription/Store.js.map +1 -1
  162. package/dist/x402/index.d.ts +1 -0
  163. package/dist/x402/index.d.ts.map +1 -1
  164. package/dist/x402/index.js +1 -0
  165. package/dist/x402/index.js.map +1 -1
  166. package/package.json +21 -10
  167. package/src/Challenge.test.ts +40 -0
  168. package/src/Challenge.ts +19 -6
  169. package/src/Mcp.ts +4 -0
  170. package/src/PaymentRequest.ts +10 -10
  171. package/src/cli/cli.test.ts +15 -15
  172. package/src/client/Mppx.test-d.ts +21 -1
  173. package/src/client/Mppx.test.ts +1 -1
  174. package/src/client/Mppx.ts +2 -2
  175. package/src/client/Transport.test.ts +225 -178
  176. package/src/client/Transport.ts +77 -83
  177. package/src/client/index.ts +14 -0
  178. package/src/client/internal/Fetch.test.ts +207 -2
  179. package/src/client/internal/Fetch.ts +52 -6
  180. package/src/client/internal/protocols/Mcp.test.ts +220 -0
  181. package/src/client/internal/protocols/Mcp.ts +162 -0
  182. package/src/client/internal/protocols/Mpp.ts +21 -0
  183. package/src/client/internal/protocols/Protocol.ts +10 -0
  184. package/src/client/internal/protocols/Shared.ts +25 -0
  185. package/src/client/internal/protocols/X402.ts +42 -0
  186. package/src/discovery/OpenApi.test.ts +1 -1
  187. package/src/evm/PublicInterface.test-d.ts +1 -1
  188. package/src/evm/client/index.ts +1 -0
  189. package/src/evm/index.ts +2 -0
  190. package/src/evm/server/Charge.test.ts +1 -1
  191. package/src/evm/server/index.ts +1 -0
  192. package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
  193. package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
  194. package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
  195. package/src/mcp/client/McpClient.ts +307 -0
  196. package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
  197. package/src/middlewares/elysia.test.ts +1 -1
  198. package/src/middlewares/express.test.ts +1 -1
  199. package/src/middlewares/hono.test.ts +1 -1
  200. package/src/middlewares/internal/mppx.test.ts +1 -1
  201. package/src/middlewares/nextjs.test.ts +1 -1
  202. package/src/proxy/Proxy.test.ts +1 -1
  203. package/src/proxy/services/anthropic.test.ts +1 -1
  204. package/src/proxy/services/openai.test.ts +1 -1
  205. package/src/proxy/services/stripe.test.ts +1 -1
  206. package/src/server/Mppx.authorize.test.ts +1 -1
  207. package/src/server/Mppx.test-d.ts +1 -1
  208. package/src/server/Mppx.test.ts +20 -2
  209. package/src/server/Mppx.ts +14 -1
  210. package/src/server/Transport.test.ts +6 -6
  211. package/src/server/Transport.ts +1 -1
  212. package/src/stripe/Charge.integration.test.ts +1 -1
  213. package/src/stripe/client/Charge.test.ts +1 -1
  214. package/src/stripe/server/Charge.test.ts +1 -1
  215. package/src/stripe/server/internal/html/package.json +1 -1
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/Proof.conformance.test.ts +146 -0
  218. package/src/tempo/Proof.test-d.ts +15 -0
  219. package/src/tempo/Proof.ts +52 -1
  220. package/src/tempo/Subscription.integration.test.ts +1 -1
  221. package/src/tempo/client/Charge.test.ts +173 -0
  222. package/src/tempo/client/Charge.ts +65 -36
  223. package/src/tempo/client/Methods.ts +4 -2
  224. package/src/tempo/client/ResolveAccount.ts +46 -0
  225. package/src/tempo/internal/fee-payer.test.ts +65 -10
  226. package/src/tempo/internal/fee-payer.ts +42 -6
  227. package/src/tempo/internal/proof.test.ts +12 -4
  228. package/src/tempo/internal/proof.ts +55 -6
  229. package/src/tempo/legacy/client/SessionManager.ts +11 -3
  230. package/src/tempo/legacy/server/Session.test.ts +91 -26
  231. package/src/tempo/server/Charge.test.ts +388 -17
  232. package/src/tempo/server/Charge.ts +46 -24
  233. package/src/tempo/server/Methods.ts +4 -2
  234. package/src/tempo/server/Subscription.test.ts +465 -3
  235. package/src/tempo/server/Subscription.ts +174 -19
  236. package/src/tempo/server/internal/html/package.json +2 -2
  237. package/src/tempo/server/internal/html.gen.ts +1 -1
  238. package/src/tempo/session/client/ChannelOps.ts +5 -19
  239. package/src/tempo/session/client/ChannelStore.ts +111 -0
  240. package/src/tempo/session/client/CredentialState.test.ts +206 -62
  241. package/src/tempo/session/client/CredentialState.ts +58 -73
  242. package/src/tempo/session/client/Session.test.ts +41 -1
  243. package/src/tempo/session/client/Session.ts +36 -10
  244. package/src/tempo/session/client/SessionManager.test.ts +154 -65
  245. package/src/tempo/session/client/SessionManager.ts +141 -235
  246. package/src/tempo/session/client/index.ts +8 -5
  247. package/src/tempo/session/precompile/Voucher.test.ts +45 -7
  248. package/src/tempo/session/precompile/Voucher.ts +27 -25
  249. package/src/tempo/session/server/Session.test.ts +4 -4
  250. package/src/tempo/session/server/Settlement.test.ts +88 -1
  251. package/src/tempo/session/server/Settlement.ts +2 -1
  252. package/src/tempo/session/server/Sse.ts +0 -2
  253. package/src/tempo/session/server/Ws.ts +0 -4
  254. package/src/tempo/subscription/Store.ts +27 -9
  255. package/src/x402/Exact.e2e.test.ts +1 -1
  256. package/src/x402/PublicInterface.test-d.ts +1 -1
  257. package/src/x402/index.ts +1 -0
  258. package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
  259. package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
  260. package/dist/mcp-sdk/client/McpClient.js +0 -118
  261. package/dist/mcp-sdk/client/McpClient.js.map +0 -1
  262. package/dist/mcp-sdk/client/index.d.ts.map +0 -1
  263. package/dist/mcp-sdk/client/index.js.map +0 -1
  264. package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
  265. package/dist/mcp-sdk/server/Transport.js.map +0 -1
  266. package/dist/mcp-sdk/server/index.d.ts.map +0 -1
  267. package/dist/mcp-sdk/server/index.js.map +0 -1
  268. package/src/mcp-sdk/client/McpClient.ts +0 -228
  269. /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
  270. /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
  271. /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
  272. /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
  273. /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
  274. /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
  275. /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
  276. /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
  277. /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
  278. /package/src/{mcp-sdk → mcp}/server/index.ts +0 -0
@@ -6,7 +6,7 @@ import { describe, expectTypeOf, test } from 'vp/test'
6
6
  import * as McpClient from './McpClient.js'
7
7
 
8
8
  describe('McpClient.wrap', () => {
9
- test('returns wrapped client with callTool', () => {
9
+ test('returns the original client with payment-aware callTool', () => {
10
10
  const client = {} as Client
11
11
  const wrapped = McpClient.wrap(client, {
12
12
  methods: [
@@ -16,6 +16,9 @@ describe('McpClient.wrap', () => {
16
16
  ],
17
17
  })
18
18
 
19
+ expectTypeOf(wrapped).toEqualTypeOf<
20
+ McpClient.wrap.McpClient<Client, readonly [ReturnType<typeof tempo>]>
21
+ >()
19
22
  expectTypeOf(wrapped.callTool).toBeFunction()
20
23
  expectTypeOf(wrapped.callTool).returns.toExtend<Promise<McpClient.CallToolResult>>()
21
24
  })
@@ -50,19 +53,36 @@ describe('McpClient.wrap', () => {
50
53
  expectTypeOf(wrapped.customProp).toEqualTypeOf<string>()
51
54
  })
52
55
 
53
- test('callTool accepts context when method has context', () => {
56
+ test('callTool keeps the MCP SDK result schema and options positions', () => {
57
+ const client = {} as Client
58
+ const wrapped = McpClient.wrap(client, {
59
+ methods: [
60
+ tempo({
61
+ account: {} as Account,
62
+ }),
63
+ ],
64
+ })
65
+
66
+ expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' })
67
+ expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, undefined, {})
68
+ expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, undefined, {
69
+ timeout: 5000,
70
+ })
71
+ })
72
+
73
+ test('callTool accepts context in the options position', () => {
54
74
  const client = {} as Client
55
75
  const wrapped = McpClient.wrap(client, {
56
76
  methods: [tempo({})],
57
77
  })
58
78
 
59
- expectTypeOf(wrapped.callTool).toBeCallableWith(
60
- { name: 'tool' },
61
- { context: { account: {} as Account } },
62
- )
79
+ expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, undefined, {
80
+ context: { account: {} as Account },
81
+ timeout: 5000,
82
+ })
63
83
  })
64
84
 
65
- test('callTool context is optional when account provided at creation', () => {
85
+ test('callTool accepts a per-call approval hook in the options position', () => {
66
86
  const client = {} as Client
67
87
  const wrapped = McpClient.wrap(client, {
68
88
  methods: [
@@ -72,16 +92,12 @@ describe('McpClient.wrap', () => {
72
92
  ],
73
93
  })
74
94
 
75
- expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' })
76
- expectTypeOf(wrapped.callTool).toBeCallableWith(null, { name: 'tool' })
77
- expectTypeOf(wrapped.callTool).toBeCallableWith(() => true, { name: 'tool' })
78
- expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, {})
79
- expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, { timeout: 5000 })
80
- expectTypeOf(wrapped.callTool).toBeCallableWith(
81
- async (challenge) => challenge.intent === 'charge',
82
- { name: 'tool' },
83
- { timeout: 5000 },
84
- )
95
+ expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, undefined, {
96
+ onPaymentRequired: async (challenge) => challenge.intent === 'charge',
97
+ })
98
+ expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, undefined, {
99
+ onPaymentRequired: null,
100
+ })
85
101
  })
86
102
 
87
103
  test('callTool result includes receipt', () => {
@@ -97,6 +113,16 @@ describe('McpClient.wrap', () => {
97
113
  expectTypeOf(wrapped.callTool({} as never)).resolves.toHaveProperty('receipt')
98
114
  expectTypeOf(wrapped.callTool({} as never)).resolves.toHaveProperty('content')
99
115
  })
116
+
117
+ test('can store an inferred client as the exported client type', () => {
118
+ const client = {} as Client
119
+
120
+ const wrapped = McpClient.wrap(client, {
121
+ methods: [tempo({ account: {} as Account })],
122
+ })
123
+
124
+ expectTypeOf(wrapped).toMatchTypeOf<McpClient.wrap.McpClient>()
125
+ })
100
126
  })
101
127
 
102
128
  describe('McpClient.wrap.McpClient', () => {
@@ -108,10 +134,11 @@ describe('McpClient.wrap.McpClient', () => {
108
134
  })
109
135
 
110
136
  describe('McpClient.wrap.CallToolOptions', () => {
111
- test('has context and timeout properties', () => {
137
+ test('has context, approval hook, and timeout properties', () => {
112
138
  type Options = McpClient.wrap.CallToolOptions
113
139
 
114
140
  expectTypeOf<Options>().toHaveProperty('context')
141
+ expectTypeOf<Options>().toHaveProperty('onPaymentRequired')
115
142
  expectTypeOf<Options>().toHaveProperty('timeout')
116
143
  })
117
144
  })
@@ -14,7 +14,31 @@ import * as McpServer_transport from '../server/Transport.js'
14
14
  import * as McpClient from './McpClient.js'
15
15
 
16
16
  const realm = 'api.example.com'
17
- const secretKey = 'test-secret-key'
17
+ const secretKey = 'test-secret-key-test-secret-key-32'
18
+
19
+ function createChallenge() {
20
+ return Challenge.fromMethod(Methods.charge, {
21
+ realm,
22
+ secretKey,
23
+ expires: new Date(Date.now() + 60_000).toISOString(),
24
+ request: {
25
+ amount: '1',
26
+ currency: asset,
27
+ decimals: 6,
28
+ recipient: accounts[0].address,
29
+ },
30
+ })
31
+ }
32
+
33
+ function createReceipt(challenge: Challenge.Challenge): core_Mcp.Receipt {
34
+ return {
35
+ challengeId: challenge.id,
36
+ method: 'tempo',
37
+ reference: 'test',
38
+ status: 'success',
39
+ timestamp: new Date().toISOString(),
40
+ }
41
+ }
18
42
 
19
43
  describe('McpClient.wrap', () => {
20
44
  let client: Client
@@ -97,10 +121,9 @@ describe('McpClient.wrap', () => {
97
121
  ],
98
122
  })
99
123
 
100
- const result = await mcp.callTool(
101
- { name: 'premium_tool', arguments: {} },
102
- { context: { account: accounts[1] } },
103
- )
124
+ const result = await mcp.callTool({ name: 'premium_tool', arguments: {} }, undefined, {
125
+ context: { account: accounts[1] },
126
+ })
104
127
 
105
128
  expect(result.content).toEqual([{ type: 'text', text: 'Premium tool executed' }])
106
129
  expect(result.receipt?.status).toBe('success')
@@ -122,6 +145,25 @@ describe('McpClient.wrap', () => {
122
145
  expect(result.receipt).toBeUndefined()
123
146
  })
124
147
 
148
+ test('behavior: does not forward empty options', async () => {
149
+ const rawCallTool = vi.fn(async () => ({
150
+ content: [{ type: 'text' as const, text: 'Free tool executed' }],
151
+ }))
152
+ const mcp = McpClient.wrap(
153
+ { callTool: rawCallTool as Client['callTool'] },
154
+ { methods: [Method.toClient(Methods.charge, { createCredential: vi.fn() })] },
155
+ )
156
+
157
+ const result = await mcp.callTool({ name: 'free_tool', arguments: {} }, undefined, {})
158
+
159
+ expect(result.content).toEqual([{ type: 'text', text: 'Free tool executed' }])
160
+ expect(rawCallTool).toHaveBeenCalledWith(
161
+ { name: 'free_tool', arguments: {} },
162
+ undefined,
163
+ undefined,
164
+ )
165
+ })
166
+
125
167
  test('behavior: throws when no account provided', async () => {
126
168
  const mcp = McpClient.wrap(client, {
127
169
  methods: [
@@ -226,6 +268,170 @@ describe('McpClient.wrap', () => {
226
268
  })
227
269
  })
228
270
 
271
+ describe('McpClient.wrap (in-place)', () => {
272
+ test('default: mutates the existing client and handles payment', async () => {
273
+ const challenge = createChallenge()
274
+ const createCredential = vi.fn(async ({ challenge }) =>
275
+ Credential.serialize({
276
+ challenge,
277
+ payload: { signature: '0xsignature', type: 'transaction' },
278
+ }),
279
+ )
280
+ const rawCallTool = vi
281
+ .fn()
282
+ .mockRejectedValueOnce(
283
+ new McpError(core_Mcp.paymentRequiredCode, 'Payment Required', {
284
+ httpStatus: 402,
285
+ challenges: [challenge],
286
+ }),
287
+ )
288
+ .mockResolvedValueOnce({
289
+ _meta: { [core_Mcp.receiptMetaKey]: createReceipt(challenge) },
290
+ content: [{ type: 'text', text: 'Premium tool executed' }],
291
+ })
292
+ const fakeClient = { callTool: rawCallTool as Client['callTool'] }
293
+
294
+ const wrapped = McpClient.wrap(fakeClient, {
295
+ methods: [Method.toClient(Methods.charge, { createCredential })],
296
+ })
297
+
298
+ expect(wrapped).toBe(fakeClient)
299
+
300
+ const result = await wrapped.callTool({ name: 'premium_tool', arguments: {} })
301
+
302
+ expect(result.content).toEqual([{ type: 'text', text: 'Premium tool executed' }])
303
+ expect(result.isError).toBeUndefined()
304
+ expect(result.receipt).toBeDefined()
305
+ expect(result.receipt?.status).toBe('success')
306
+ expect(rawCallTool).toHaveBeenCalledTimes(2)
307
+ expect(createCredential).toHaveBeenCalledOnce()
308
+ })
309
+
310
+ test('behavior: preserves the MCP SDK callTool argument shape', async () => {
311
+ const rawCallTool = vi.fn(async () => ({
312
+ content: [{ type: 'text' as const, text: 'Free tool executed' }],
313
+ }))
314
+ const createCredential = vi.fn()
315
+ const fakeClient = { callTool: rawCallTool as Client['callTool'] }
316
+
317
+ const wrapped = McpClient.wrap(fakeClient, {
318
+ methods: [Method.toClient(Methods.charge, { createCredential })],
319
+ })
320
+
321
+ const result = await wrapped.callTool({ name: 'free_tool', arguments: {} }, undefined, {
322
+ timeout: 30_000,
323
+ })
324
+
325
+ expect(result.content).toEqual([{ type: 'text', text: 'Free tool executed' }])
326
+ expect(result.receipt).toBeUndefined()
327
+ expect(rawCallTool).toHaveBeenCalledWith({ name: 'free_tool', arguments: {} }, undefined, {
328
+ timeout: 30_000,
329
+ })
330
+ expect(createCredential).not.toHaveBeenCalled()
331
+ })
332
+
333
+ test('behavior: strips payment context from MCP SDK request options', async () => {
334
+ const rawCallTool = vi.fn(async () => ({
335
+ content: [{ type: 'text' as const, text: 'Free tool executed' }],
336
+ }))
337
+ const fakeClient = { callTool: rawCallTool as Client['callTool'] }
338
+
339
+ const wrapped = McpClient.wrap(fakeClient, {
340
+ methods: [tempo_client({})],
341
+ })
342
+
343
+ await wrapped.callTool({ name: 'free_tool', arguments: {} }, undefined, {
344
+ context: { account: accounts[1] },
345
+ timeout: 30_000,
346
+ })
347
+
348
+ expect(rawCallTool).toHaveBeenCalledWith({ name: 'free_tool', arguments: {} }, undefined, {
349
+ timeout: 30_000,
350
+ })
351
+ })
352
+
353
+ test('behavior: re-wrapping replaces config without stacking wrappers', async () => {
354
+ const challenge = createChallenge()
355
+
356
+ const rawCallTool = vi
357
+ .fn()
358
+ .mockRejectedValueOnce(
359
+ new McpError(core_Mcp.paymentRequiredCode, 'Payment Required', {
360
+ httpStatus: 402,
361
+ challenges: [challenge],
362
+ }),
363
+ )
364
+ .mockResolvedValueOnce({
365
+ _meta: { [core_Mcp.receiptMetaKey]: createReceipt(challenge) },
366
+ content: [{ type: 'text', text: 'paid' }],
367
+ })
368
+
369
+ const fakeClient = { callTool: rawCallTool as Client['callTool'] }
370
+ const staleCreateCredential = vi.fn(async () => {
371
+ throw new Error('stale config used')
372
+ })
373
+ const freshCreateCredential = vi.fn(async ({ challenge }) =>
374
+ Credential.serialize({
375
+ challenge,
376
+ payload: { signature: '0xsignature', type: 'transaction' },
377
+ }),
378
+ )
379
+
380
+ McpClient.wrap(fakeClient, {
381
+ methods: [Method.toClient(Methods.charge, { createCredential: staleCreateCredential })],
382
+ })
383
+ const wrapped = McpClient.wrap(fakeClient, {
384
+ methods: [Method.toClient(Methods.charge, { createCredential: freshCreateCredential })],
385
+ })
386
+
387
+ const result = await wrapped.callTool({ name: 'premium_tool', arguments: {} })
388
+
389
+ expect(result.content).toEqual([{ type: 'text', text: 'paid' }])
390
+ expect(result.receipt?.status).toBe('success')
391
+ expect(staleCreateCredential).not.toHaveBeenCalled()
392
+ expect(freshCreateCredential).toHaveBeenCalledOnce()
393
+ expect(rawCallTool).toHaveBeenCalledTimes(2)
394
+ })
395
+
396
+ test('behavior: handles payment challenge metadata returned as a tool result', async () => {
397
+ const challenge = createChallenge()
398
+ const createCredential = vi.fn(async ({ challenge }) =>
399
+ Credential.serialize({
400
+ challenge,
401
+ payload: { signature: '0xsignature', type: 'transaction' },
402
+ }),
403
+ )
404
+ const rawCallTool = vi
405
+ .fn()
406
+ .mockResolvedValueOnce({
407
+ _meta: {
408
+ [core_Mcp.paymentRequiredMetaKey]: {
409
+ challenges: [challenge],
410
+ httpStatus: 402,
411
+ },
412
+ },
413
+ content: [{ type: 'text', text: 'Payment Required' }],
414
+ isError: true,
415
+ })
416
+ .mockResolvedValueOnce({
417
+ _meta: { [core_Mcp.receiptMetaKey]: createReceipt(challenge) },
418
+ content: [{ type: 'text', text: 'Premium tool executed' }],
419
+ })
420
+
421
+ const wrapped = McpClient.wrap(
422
+ { callTool: rawCallTool as Client['callTool'] },
423
+ { methods: [Method.toClient(Methods.charge, { createCredential })] },
424
+ )
425
+
426
+ const result = await wrapped.callTool({ name: 'premium_tool', arguments: {} })
427
+
428
+ expect(result.content).toEqual([{ type: 'text', text: 'Premium tool executed' }])
429
+ expect(result.receipt?.status).toBe('success')
430
+ expect(createCredential).toHaveBeenCalledOnce()
431
+ expect(rawCallTool).toHaveBeenCalledTimes(2)
432
+ })
433
+ })
434
+
229
435
  describe('isPaymentRequiredError', () => {
230
436
  test('returns true for McpError with payment code and challenges', () => {
231
437
  const error = new McpError(core_Mcp.paymentRequiredCode, 'Payment Required', {
@@ -0,0 +1,307 @@
1
+ import type { Client } from '@modelcontextprotocol/sdk/client/index.js'
2
+ import type { McpError } from '@modelcontextprotocol/sdk/types.js'
3
+
4
+ import * as Challenge from '../../Challenge.js'
5
+ import * as Credential from '../../Credential.js'
6
+ import * as Expires from '../../Expires.js'
7
+ import * as AcceptPayment from '../../internal/AcceptPayment.js'
8
+ import * as core_Mcp from '../../Mcp.js'
9
+ import type * as Method from '../../Method.js'
10
+ import * as z from '../../zod.js'
11
+
12
+ type AnyClient = Method.Client<any, any>
13
+ type Methods = readonly (Method.AnyClient | readonly Method.AnyClient[])[]
14
+ type DefaultMethods = readonly [Method.AnyClient | readonly Method.AnyClient[]]
15
+ type CallToolParams = Parameters<Client['callTool']>[0]
16
+ type CallToolResultSchema = Parameters<Client['callTool']>[1]
17
+ type CallToolRequestOptions = Parameters<Client['callTool']>[2]
18
+ type PaymentRequiredData = NonNullable<core_Mcp.ErrorObject['data']>
19
+
20
+ const MPPX_MCP_CLIENT_WRAPPER = Symbol.for('mppx.mcp.client.wrapper')
21
+
22
+ export type OnPaymentRequired = (challenge: Challenge.Challenge) => boolean | Promise<boolean>
23
+
24
+ /**
25
+ * Result of a tool call with payment handling.
26
+ * Extends the SDK's callTool return type with an optional payment receipt.
27
+ */
28
+ export type CallToolResult = Awaited<ReturnType<Client['callTool']>> & {
29
+ /** Payment receipt if payment was made. */
30
+ receipt: core_Mcp.Receipt | undefined
31
+ }
32
+
33
+ /**
34
+ * Adds automatic payment handling to an MCP SDK client.
35
+ *
36
+ * The client's `callTool` method is replaced in place and the same reference
37
+ * is returned, so surfaces that keep using the original client become
38
+ * payment-aware — including when another SDK owns the client reference (e.g.
39
+ * Cloudflare Agents). The MCP SDK `callTool(params, resultSchema?, options?)`
40
+ * signature is preserved; pass a method's `context` or a per-call
41
+ * `onPaymentRequired` approval hook via the options argument, where they are
42
+ * stripped before the remaining request options are forwarded to the SDK.
43
+ * Payment challenges are handled whether they arrive as payment-required
44
+ * errors or as tool results carrying payment-required metadata. Calling
45
+ * `wrap()` again replaces the payment configuration.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { Client } from '@modelcontextprotocol/sdk/client'
50
+ * import { tempo } from 'mppx/client'
51
+ * import { McpClient } from 'mppx/mcp/client'
52
+ * import { privateKeyToAccount } from 'viem/accounts'
53
+ *
54
+ * const client = new Client({ name: 'my-client', version: '1.0.0' })
55
+ * await client.connect(transport)
56
+ *
57
+ * McpClient.wrap(client, {
58
+ * methods: [
59
+ * tempo({
60
+ * account: privateKeyToAccount('0x...'),
61
+ * }),
62
+ * ],
63
+ * })
64
+ *
65
+ * // Automatically handles payment challenges
66
+ * const result = await client.callTool({ name: 'premium_tool', arguments: {} })
67
+ * console.log(result.content, result.receipt)
68
+ * ```
69
+ */
70
+ export function wrap<const client extends Pick<Client, 'callTool'>, const methods extends Methods>(
71
+ client: client,
72
+ config: wrap.Config<methods>,
73
+ ): wrap.McpClient<client, methods> {
74
+ const target = client as client & { [MPPX_MCP_CLIENT_WRAPPER]?: Client['callTool'] }
75
+ const originalCallTool = target[MPPX_MCP_CLIENT_WRAPPER] ?? target.callTool
76
+ const callTool = createPaymentAwareCallTool(originalCallTool.bind(client), config)
77
+
78
+ Object.defineProperty(target, MPPX_MCP_CLIENT_WRAPPER, {
79
+ configurable: true,
80
+ value: originalCallTool,
81
+ })
82
+
83
+ Object.defineProperty(target, 'callTool', {
84
+ configurable: true,
85
+ enumerable: false,
86
+ value: (
87
+ params: CallToolParams,
88
+ resultSchema?: CallToolResultSchema,
89
+ options?: wrap.CallToolOptions<methods>,
90
+ ) => {
91
+ const { context, onPaymentRequired, ...requestOptions } =
92
+ options ?? ({} as wrap.CallToolOptions<methods>)
93
+ return callTool(params, {
94
+ context,
95
+ onPaymentRequired:
96
+ onPaymentRequired === null ? undefined : (onPaymentRequired ?? config.onPaymentRequired),
97
+ requestOptions: Object.keys(requestOptions).length
98
+ ? (requestOptions as CallToolRequestOptions)
99
+ : undefined,
100
+ resultSchema,
101
+ })
102
+ },
103
+ writable: true,
104
+ })
105
+
106
+ return target as unknown as wrap.McpClient<client, methods>
107
+ }
108
+
109
+ export declare namespace wrap {
110
+ type Config<methods extends Methods = Methods> = {
111
+ /** Optional approval hook called before creating a payment credential. */
112
+ onPaymentRequired?: OnPaymentRequired
113
+ /** Filters and sorts supported Challenges before Credential creation. */
114
+ orderChallenges?: AcceptPayment.OrderChallenges<FlattenMethods<methods>> | undefined
115
+ /** Client-declared supported payment methods, keyed by typed `method/intent` strings. */
116
+ paymentPreferences?: AcceptPayment.Config<FlattenMethods<methods>> | undefined
117
+ /** Array of methods to use. Accepts individual clients or tuples (e.g. from `tempo()`). */
118
+ methods: methods
119
+ }
120
+
121
+ type McpClient<
122
+ client extends Pick<Client, 'callTool'> = Pick<Client, 'callTool'>,
123
+ methods extends Methods = DefaultMethods,
124
+ > = Omit<client, 'callTool'> & {
125
+ /** Call a tool with automatic payment handling. Preserves the MCP SDK signature. */
126
+ callTool: (
127
+ params: CallToolParams,
128
+ resultSchema?: CallToolResultSchema,
129
+ options?: CallToolOptions<methods>,
130
+ ) => Promise<CallToolResult>
131
+ }
132
+
133
+ type CallToolOptions<methods extends Methods = DefaultMethods> = CallToolRequestOptions & {
134
+ /** Context to pass to the method intent's createCredential. */
135
+ context?: AnyContextForMethods<methods>
136
+ /** Per-call approval hook; overrides the configured hook. Pass `null` to bypass it. */
137
+ onPaymentRequired?: OnPaymentRequired | null
138
+ }
139
+ }
140
+
141
+ /** Minimal wire shape of payment-required data; challenges are validated, extra fields pass through. */
142
+ const PaymentRequiredSchema = z.object({
143
+ challenges: z.array(Challenge.Schema).check(z.minLength(1)),
144
+ })
145
+
146
+ /**
147
+ * Checks if an error is a payment required error.
148
+ */
149
+ export function isPaymentRequiredError(
150
+ error: unknown,
151
+ ): error is McpError & { data: PaymentRequiredData } {
152
+ if (typeof error !== 'object' || error === null) return false
153
+ if (!('code' in error) || !('message' in error)) return false
154
+ if ((error as { code: unknown }).code !== core_Mcp.paymentRequiredCode) return false
155
+ return isPaymentRequiredData((error as { data?: unknown }).data)
156
+ }
157
+
158
+ /** @internal */
159
+ async function createCredential<methods extends readonly Method.AnyClient[]>(
160
+ challenge: Challenge.Challenge,
161
+ config: {
162
+ context?: unknown
163
+ methods: methods
164
+ },
165
+ ): Promise<string> {
166
+ const { context, methods } = config
167
+
168
+ const mi = methods.find((m) => m.name === challenge.method && m.intent === challenge.intent)
169
+ if (!mi)
170
+ throw new Error(
171
+ `No method found for "${challenge.method}.${challenge.intent}". Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`,
172
+ )
173
+
174
+ if (challenge.expires) Expires.assert(challenge.expires, challenge.id)
175
+
176
+ const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined
177
+ return mi.createCredential(
178
+ parsedContext !== undefined ? { challenge, context: parsedContext } : ({ challenge } as never),
179
+ )
180
+ }
181
+
182
+ /** Normalized per-call inputs for the payment-aware adapter. @internal */
183
+ type CallToolCall = {
184
+ context?: unknown
185
+ onPaymentRequired?: OnPaymentRequired | undefined
186
+ requestOptions?: CallToolRequestOptions | undefined
187
+ resultSchema?: CallToolResultSchema | undefined
188
+ }
189
+
190
+ function createPaymentAwareCallTool<methods extends Methods>(
191
+ callTool: Client['callTool'],
192
+ config: wrap.Config<methods>,
193
+ ): (params: CallToolParams, call: CallToolCall) => Promise<CallToolResult> {
194
+ const methods = config.methods.flat() as unknown as FlattenMethods<methods>
195
+ const paymentPreferences = AcceptPayment.resolve(methods, config.paymentPreferences)
196
+
197
+ const retryWithPayment = async (
198
+ params: CallToolParams,
199
+ call: CallToolCall,
200
+ paymentRequired: PaymentRequiredData,
201
+ cause: unknown,
202
+ ) => {
203
+ const challenges = paymentRequired.challenges
204
+ const candidates = AcceptPayment.selectChallengeCandidates(
205
+ challenges,
206
+ methods,
207
+ paymentPreferences.entries,
208
+ )
209
+ const orderedCandidates = config.orderChallenges
210
+ ? await config.orderChallenges(candidates)
211
+ : candidates
212
+ const selected = orderedCandidates[0]
213
+
214
+ if (!selected) {
215
+ const available = challenges.map((challenge) => `${challenge.method}.${challenge.intent}`)
216
+ const installed = methods.map((method) => `${method.name}.${method.intent}`)
217
+ throw new Error(
218
+ `No compatible payment method. Server offers: ${available.join(', ')}. Client has: ${installed.join(', ')}`,
219
+ { cause },
220
+ )
221
+ }
222
+
223
+ if (selected.challenge.expires)
224
+ Expires.assert(selected.challenge.expires, selected.challenge.id)
225
+
226
+ if (call.onPaymentRequired) {
227
+ const approved = await call.onPaymentRequired(selected.challenge)
228
+ if (!approved) throw new Error('Payment declined.', { cause })
229
+ }
230
+
231
+ const credential = await createCredential(selected.challenge, {
232
+ context: call.context,
233
+ methods,
234
+ })
235
+ const parsed = Credential.deserialize(credential)
236
+
237
+ const retryResult = await callTool(
238
+ {
239
+ ...params,
240
+ _meta: {
241
+ ...params._meta,
242
+ [core_Mcp.credentialMetaKey]: parsed,
243
+ },
244
+ },
245
+ call.resultSchema,
246
+ call.requestOptions,
247
+ )
248
+
249
+ return withReceipt(retryResult)
250
+ }
251
+
252
+ return async (params, call) => {
253
+ try {
254
+ const result = await callTool(params, call.resultSchema, call.requestOptions)
255
+ const paymentRequired = getPaymentRequiredMeta(result)
256
+ if (paymentRequired) return retryWithPayment(params, call, paymentRequired, result)
257
+ return withReceipt(result)
258
+ } catch (error) {
259
+ if (!isPaymentRequiredError(error)) throw error
260
+ return retryWithPayment(params, call, error.data, error)
261
+ }
262
+ }
263
+ }
264
+
265
+ function getPaymentRequiredMeta(
266
+ result: Awaited<ReturnType<Client['callTool']>>,
267
+ ): PaymentRequiredData | undefined {
268
+ const data = result._meta?.[core_Mcp.paymentRequiredMetaKey]
269
+ return isPaymentRequiredData(data) ? data : undefined
270
+ }
271
+
272
+ function isPaymentRequiredData(value: unknown): value is PaymentRequiredData {
273
+ return PaymentRequiredSchema.safeParse(value).success
274
+ }
275
+
276
+ function withReceipt(result: Awaited<ReturnType<Client['callTool']>>): CallToolResult {
277
+ return {
278
+ ...result,
279
+ receipt: result._meta?.[core_Mcp.receiptMetaKey] as core_Mcp.Receipt | undefined,
280
+ }
281
+ }
282
+
283
+ /** Union of all context types from all methods that have context schemas. */
284
+ type AnyContextFor<methods extends readonly AnyClient[]> = {
285
+ [key in keyof methods]: methods[key] extends Method.Client<any, infer context>
286
+ ? context extends z.ZodMiniType
287
+ ? z.input<context>
288
+ : undefined
289
+ : undefined
290
+ }[number]
291
+
292
+ /** Union of all context types across a methods config, flattening tuples. @internal */
293
+ type AnyContextForMethods<methods extends Methods> =
294
+ FlattenMethods<methods> extends infer flattened extends readonly AnyClient[]
295
+ ? AnyContextFor<flattened>
296
+ : never
297
+
298
+ type FlattenMethods<methods extends Methods> = methods extends readonly [
299
+ infer head,
300
+ ...infer tail extends Methods,
301
+ ]
302
+ ? head extends readonly Method.AnyClient[]
303
+ ? readonly [...head, ...FlattenMethods<tail>]
304
+ : head extends Method.AnyClient
305
+ ? readonly [head, ...FlattenMethods<tail>]
306
+ : never
307
+ : readonly []
@@ -48,7 +48,9 @@ describe('MCP client payment approval', () => {
48
48
  methods: [Method.toClient(Methods.charge, { createCredential })],
49
49
  })
50
50
 
51
- const result = await mcp.callTool(onPaymentRequired, { name: 'paid_tool', arguments: {} })
51
+ const result = await mcp.callTool({ name: 'paid_tool', arguments: {} }, undefined, {
52
+ onPaymentRequired,
53
+ })
52
54
 
53
55
  expect(result.content).toEqual([{ type: 'text', text: 'ok' }])
54
56
  expect(onPaymentRequired).toHaveBeenCalledWith(challenge)
@@ -82,9 +84,9 @@ describe('MCP client payment approval', () => {
82
84
  methods: [Method.toClient(Methods.charge, { createCredential })],
83
85
  })
84
86
 
85
- await expect(mcp.callTool(() => false, { name: 'paid_tool' })).rejects.toThrow(
86
- 'Payment declined.',
87
- )
87
+ await expect(
88
+ mcp.callTool({ name: 'paid_tool' }, undefined, { onPaymentRequired: () => false }),
89
+ ).rejects.toThrow('Payment declined.')
88
90
  expect(createCredential).not.toHaveBeenCalled()
89
91
  })
90
92
 
@@ -122,7 +124,9 @@ describe('MCP client payment approval', () => {
122
124
  onPaymentRequired,
123
125
  })
124
126
 
125
- await expect(mcp.callTool(null, { name: 'paid_tool' })).resolves.toMatchObject({
127
+ await expect(
128
+ mcp.callTool({ name: 'paid_tool' }, undefined, { onPaymentRequired: null }),
129
+ ).resolves.toMatchObject({
126
130
  content: [{ type: 'text', text: 'ok' }],
127
131
  })
128
132
  expect(onPaymentRequired).not.toHaveBeenCalled()