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.
- package/CHANGELOG.md +33 -0
- package/README.md +20 -11
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +18 -6
- package/dist/Challenge.js.map +1 -1
- package/dist/Mcp.d.ts +3 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +2 -0
- package/dist/Mcp.js.map +1 -1
- package/dist/PaymentRequest.d.ts +10 -10
- package/dist/PaymentRequest.js +8 -8
- package/dist/client/Mppx.js +2 -2
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +11 -16
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +55 -75
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +46 -7
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/protocols/Mcp.d.ts +7 -0
- package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
- package/dist/client/internal/protocols/Mcp.js +159 -0
- package/dist/client/internal/protocols/Mcp.js.map +1 -0
- package/dist/client/internal/protocols/Mpp.d.ts +4 -0
- package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
- package/dist/client/internal/protocols/Mpp.js +18 -0
- package/dist/client/internal/protocols/Mpp.js.map +1 -0
- package/dist/client/internal/protocols/Protocol.d.ts +10 -0
- package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
- package/dist/client/internal/protocols/Protocol.js +2 -0
- package/dist/client/internal/protocols/Protocol.js.map +1 -0
- package/dist/client/internal/protocols/Shared.d.ts +5 -0
- package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
- package/dist/client/internal/protocols/Shared.js +20 -0
- package/dist/client/internal/protocols/Shared.js.map +1 -0
- package/dist/client/internal/protocols/X402.d.ts +8 -0
- package/dist/client/internal/protocols/X402.d.ts.map +1 -0
- package/dist/client/internal/protocols/X402.js +39 -0
- package/dist/client/internal/protocols/X402.js.map +1 -0
- package/dist/evm/client/index.d.ts +1 -0
- package/dist/evm/client/index.d.ts.map +1 -1
- package/dist/evm/client/index.js +1 -0
- package/dist/evm/client/index.js.map +1 -1
- package/dist/evm/index.d.ts +2 -0
- package/dist/evm/index.d.ts.map +1 -1
- package/dist/evm/index.js +2 -0
- package/dist/evm/index.js.map +1 -1
- package/dist/evm/server/index.d.ts +1 -0
- package/dist/evm/server/index.d.ts.map +1 -1
- package/dist/evm/server/index.js +1 -0
- package/dist/evm/server/index.js.map +1 -1
- package/dist/mcp/client/McpClient.d.ts +101 -0
- package/dist/mcp/client/McpClient.d.ts.map +1 -0
- package/dist/mcp/client/McpClient.js +162 -0
- package/dist/mcp/client/McpClient.js.map +1 -0
- package/dist/mcp/client/index.d.ts.map +1 -0
- package/dist/mcp/client/index.js.map +1 -0
- package/dist/mcp/server/Transport.d.ts.map +1 -0
- package/dist/mcp/server/Transport.js.map +1 -0
- package/dist/mcp/server/index.d.ts.map +1 -0
- package/dist/mcp/server/index.js.map +1 -0
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +9 -0
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +1 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +1 -1
- package/dist/server/Transport.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/Proof.d.ts +85 -1
- package/dist/tempo/Proof.d.ts.map +1 -1
- package/dist/tempo/Proof.js +35 -0
- package/dist/tempo/Proof.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +13 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +38 -25
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +5 -3
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Methods.js +4 -2
- package/dist/tempo/client/Methods.js.map +1 -1
- package/dist/tempo/client/ResolveAccount.d.ts +40 -0
- package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
- package/dist/tempo/client/ResolveAccount.js +2 -0
- package/dist/tempo/client/ResolveAccount.js.map +1 -0
- package/dist/tempo/internal/fee-payer.d.ts +9 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +35 -6
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/proof.d.ts +71 -5
- package/dist/tempo/internal/proof.d.ts.map +1 -1
- package/dist/tempo/internal/proof.js +42 -6
- package/dist/tempo/internal/proof.js.map +1 -1
- package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/legacy/client/SessionManager.js +10 -3
- package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +42 -18
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +4 -2
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -2
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +10 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +135 -23
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +7 -10
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
- package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/session/client/ChannelStore.js +63 -0
- package/dist/tempo/session/client/ChannelStore.js.map +1 -0
- package/dist/tempo/session/client/CredentialState.d.ts +7 -24
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +51 -49
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +8 -2
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +22 -8
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +4 -40
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +124 -174
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/index.d.ts +3 -4
- package/dist/tempo/session/client/index.d.ts.map +1 -1
- package/dist/tempo/session/client/index.js +1 -0
- package/dist/tempo/session/client/index.js.map +1 -1
- package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
- package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Voucher.js +24 -25
- package/dist/tempo/session/precompile/Voucher.js.map +1 -1
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
- package/dist/tempo/session/server/Settlement.js +4 -2
- package/dist/tempo/session/server/Settlement.js.map +1 -1
- package/dist/tempo/session/server/Sse.d.ts.map +1 -1
- package/dist/tempo/session/server/Sse.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/Store.d.ts +2 -0
- package/dist/tempo/subscription/Store.d.ts.map +1 -1
- package/dist/tempo/subscription/Store.js +16 -1
- package/dist/tempo/subscription/Store.js.map +1 -1
- package/dist/x402/index.d.ts +1 -0
- package/dist/x402/index.d.ts.map +1 -1
- package/dist/x402/index.js +1 -0
- package/dist/x402/index.js.map +1 -1
- package/package.json +21 -10
- package/src/Challenge.test.ts +40 -0
- package/src/Challenge.ts +19 -6
- package/src/Mcp.ts +4 -0
- package/src/PaymentRequest.ts +10 -10
- package/src/cli/cli.test.ts +15 -15
- package/src/client/Mppx.test-d.ts +21 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Mppx.ts +2 -2
- package/src/client/Transport.test.ts +225 -178
- package/src/client/Transport.ts +77 -83
- package/src/client/index.ts +14 -0
- package/src/client/internal/Fetch.test.ts +207 -2
- package/src/client/internal/Fetch.ts +52 -6
- package/src/client/internal/protocols/Mcp.test.ts +220 -0
- package/src/client/internal/protocols/Mcp.ts +162 -0
- package/src/client/internal/protocols/Mpp.ts +21 -0
- package/src/client/internal/protocols/Protocol.ts +10 -0
- package/src/client/internal/protocols/Shared.ts +25 -0
- package/src/client/internal/protocols/X402.ts +42 -0
- package/src/discovery/OpenApi.test.ts +1 -1
- package/src/evm/PublicInterface.test-d.ts +1 -1
- package/src/evm/client/index.ts +1 -0
- package/src/evm/index.ts +2 -0
- package/src/evm/server/Charge.test.ts +1 -1
- package/src/evm/server/index.ts +1 -0
- package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
- package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
- package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
- package/src/mcp/client/McpClient.ts +307 -0
- package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
- package/src/middlewares/elysia.test.ts +1 -1
- package/src/middlewares/express.test.ts +1 -1
- package/src/middlewares/hono.test.ts +1 -1
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/nextjs.test.ts +1 -1
- package/src/proxy/Proxy.test.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +1 -1
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/stripe.test.ts +1 -1
- package/src/server/Mppx.authorize.test.ts +1 -1
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +20 -2
- package/src/server/Mppx.ts +14 -1
- package/src/server/Transport.test.ts +6 -6
- package/src/server/Transport.ts +1 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +1 -1
- package/src/stripe/server/Charge.test.ts +1 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/Proof.conformance.test.ts +146 -0
- package/src/tempo/Proof.test-d.ts +15 -0
- package/src/tempo/Proof.ts +52 -1
- package/src/tempo/Subscription.integration.test.ts +1 -1
- package/src/tempo/client/Charge.test.ts +173 -0
- package/src/tempo/client/Charge.ts +65 -36
- package/src/tempo/client/Methods.ts +4 -2
- package/src/tempo/client/ResolveAccount.ts +46 -0
- package/src/tempo/internal/fee-payer.test.ts +65 -10
- package/src/tempo/internal/fee-payer.ts +42 -6
- package/src/tempo/internal/proof.test.ts +12 -4
- package/src/tempo/internal/proof.ts +55 -6
- package/src/tempo/legacy/client/SessionManager.ts +11 -3
- package/src/tempo/legacy/server/Session.test.ts +91 -26
- package/src/tempo/server/Charge.test.ts +388 -17
- package/src/tempo/server/Charge.ts +46 -24
- package/src/tempo/server/Methods.ts +4 -2
- package/src/tempo/server/Subscription.test.ts +465 -3
- package/src/tempo/server/Subscription.ts +174 -19
- package/src/tempo/server/internal/html/package.json +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/ChannelOps.ts +5 -19
- package/src/tempo/session/client/ChannelStore.ts +111 -0
- package/src/tempo/session/client/CredentialState.test.ts +206 -62
- package/src/tempo/session/client/CredentialState.ts +58 -73
- package/src/tempo/session/client/Session.test.ts +41 -1
- package/src/tempo/session/client/Session.ts +36 -10
- package/src/tempo/session/client/SessionManager.test.ts +154 -65
- package/src/tempo/session/client/SessionManager.ts +141 -235
- package/src/tempo/session/client/index.ts +8 -5
- package/src/tempo/session/precompile/Voucher.test.ts +45 -7
- package/src/tempo/session/precompile/Voucher.ts +27 -25
- package/src/tempo/session/server/Session.test.ts +4 -4
- package/src/tempo/session/server/Settlement.test.ts +88 -1
- package/src/tempo/session/server/Settlement.ts +2 -1
- package/src/tempo/session/server/Sse.ts +0 -2
- package/src/tempo/session/server/Ws.ts +0 -4
- package/src/tempo/subscription/Store.ts +27 -9
- package/src/x402/Exact.e2e.test.ts +1 -1
- package/src/x402/PublicInterface.test-d.ts +1 -1
- package/src/x402/index.ts +1 -0
- package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
- package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
- package/dist/mcp-sdk/client/McpClient.js +0 -118
- package/dist/mcp-sdk/client/McpClient.js.map +0 -1
- package/dist/mcp-sdk/client/index.d.ts.map +0 -1
- package/dist/mcp-sdk/client/index.js.map +0 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
- package/dist/mcp-sdk/server/Transport.js.map +0 -1
- package/dist/mcp-sdk/server/index.d.ts.map +0 -1
- package/dist/mcp-sdk/server/index.js.map +0 -1
- package/src/mcp-sdk/client/McpClient.ts +0 -228
- /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
- /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
- /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
- /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
- /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
- /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
- /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
- /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
- /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
- /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
|
|
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
|
|
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
|
-
{
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
expectTypeOf(wrapped.callTool).toBeCallableWith({ name: 'tool' }, {
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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(
|
|
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(
|
|
86
|
-
'
|
|
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(
|
|
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()
|