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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Challenge, Mcp } from 'mppx'
|
|
2
|
+
import { Methods } from 'mppx/tempo'
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
|
+
|
|
5
|
+
import { mcp } from './Mcp.js'
|
|
6
|
+
|
|
7
|
+
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
8
|
+
realm: 'api.example.com',
|
|
9
|
+
secretKey: 'test-secret-key',
|
|
10
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
11
|
+
request: {
|
|
12
|
+
amount: '0.001',
|
|
13
|
+
currency: '0x20c0000000000000000000000000000000000001',
|
|
14
|
+
decimals: 6,
|
|
15
|
+
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const paymentRequired = {
|
|
20
|
+
jsonrpc: '2.0',
|
|
21
|
+
id: 1,
|
|
22
|
+
error: {
|
|
23
|
+
code: Mcp.paymentRequiredCode,
|
|
24
|
+
message: 'Payment Required',
|
|
25
|
+
data: { challenges: [challenge] },
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const request = (overrides: Partial<RequestInit> = {}) =>
|
|
30
|
+
({
|
|
31
|
+
headers: { accept: 'application/json, text/event-stream' },
|
|
32
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} }),
|
|
33
|
+
...overrides,
|
|
34
|
+
}) satisfies RequestInit
|
|
35
|
+
|
|
36
|
+
const jsonResponse = (body: unknown, contentType = 'application/json') =>
|
|
37
|
+
new Response(typeof body === 'string' ? body : JSON.stringify(body), {
|
|
38
|
+
headers: { 'content-type': contentType },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const sseResponse = (body: string) =>
|
|
42
|
+
new Response(body, { headers: { 'content-type': 'text/event-stream' } })
|
|
43
|
+
|
|
44
|
+
const sseEvent = (data: unknown, event = 'message') =>
|
|
45
|
+
`${event ? `event: ${event}\n` : ''}data: ${
|
|
46
|
+
typeof data === 'string' ? data : JSON.stringify(data)
|
|
47
|
+
}\n\n`
|
|
48
|
+
|
|
49
|
+
async function getChallengeIds(response: Response, requestInit = request()) {
|
|
50
|
+
return (await mcp().getChallenges(response, requestInit)).map((entry) => entry.id)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('mcp HTTP protocol', () => {
|
|
54
|
+
test('extracts payment challenges from JSON-RPC error JSON', async () => {
|
|
55
|
+
expect(await getChallengeIds(jsonResponse(paymentRequired))).toEqual([challenge.id])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('extracts all valid payment challenges', async () => {
|
|
59
|
+
const alternate = { ...challenge, id: 'alternate' }
|
|
60
|
+
const response = jsonResponse({
|
|
61
|
+
...paymentRequired,
|
|
62
|
+
error: {
|
|
63
|
+
...paymentRequired.error,
|
|
64
|
+
data: { challenges: [challenge, alternate] },
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(await getChallengeIds(response)).toEqual([challenge.id, alternate.id])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('parses JSON content types case-insensitively', async () => {
|
|
72
|
+
const response = jsonResponse(paymentRequired, 'Application/JSON; charset=utf-8')
|
|
73
|
+
|
|
74
|
+
expect(await getChallengeIds(response)).toEqual([challenge.id])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test.each([
|
|
78
|
+
{ body: '[', name: 'invalid JSON' },
|
|
79
|
+
{ body: [paymentRequired], name: 'JSON-RPC batch' },
|
|
80
|
+
{
|
|
81
|
+
body: { jsonrpc: '2.0', method: 'notifications/progress', params: {} },
|
|
82
|
+
name: 'notification',
|
|
83
|
+
},
|
|
84
|
+
{ body: { jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} }, name: 'request' },
|
|
85
|
+
{ body: { ...paymentRequired, jsonrpc: '2.1' }, name: 'wrong JSON-RPC version' },
|
|
86
|
+
{ body: { ...paymentRequired, id: null }, name: 'null id' },
|
|
87
|
+
{ body: { ...paymentRequired, id: 2 }, name: 'different id' },
|
|
88
|
+
{ body: { ...paymentRequired, result: {} }, name: 'result and error together' },
|
|
89
|
+
{ body: { jsonrpc: '2.0', id: 1, result: {} }, name: 'successful result' },
|
|
90
|
+
{
|
|
91
|
+
body: {
|
|
92
|
+
jsonrpc: '2.0',
|
|
93
|
+
id: 1,
|
|
94
|
+
error: { code: -32600, message: 'Invalid Request' },
|
|
95
|
+
},
|
|
96
|
+
name: 'non-payment error',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
body: {
|
|
100
|
+
jsonrpc: '2.0',
|
|
101
|
+
id: 1,
|
|
102
|
+
error: { code: `${Mcp.paymentRequiredCode}`, message: 'Payment Required' },
|
|
103
|
+
},
|
|
104
|
+
name: 'string error code',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
body: {
|
|
108
|
+
jsonrpc: '2.0',
|
|
109
|
+
id: 1,
|
|
110
|
+
error: { code: Mcp.paymentRequiredCode, message: 402 },
|
|
111
|
+
},
|
|
112
|
+
name: 'non-string error message',
|
|
113
|
+
},
|
|
114
|
+
])('ignores $name message shapes', async ({ body }) => {
|
|
115
|
+
expect(await getChallengeIds(jsonResponse(body))).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test.each([
|
|
119
|
+
{ challenges: undefined, name: 'missing challenges' },
|
|
120
|
+
{ challenges: [], name: 'empty challenges' },
|
|
121
|
+
{ challenges: challenge, name: 'non-array challenges' },
|
|
122
|
+
{ challenges: [{ ...challenge, realm: undefined }], name: 'invalid challenge' },
|
|
123
|
+
{
|
|
124
|
+
challenges: [challenge, { ...challenge, realm: undefined }],
|
|
125
|
+
name: 'partially invalid challenges',
|
|
126
|
+
},
|
|
127
|
+
])('ignores $name', async ({ challenges }) => {
|
|
128
|
+
const response = jsonResponse({
|
|
129
|
+
...paymentRequired,
|
|
130
|
+
error: {
|
|
131
|
+
...paymentRequired.error,
|
|
132
|
+
data: challenges === undefined ? undefined : { challenges },
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(await getChallengeIds(response)).toEqual([])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('matches string request and response ids without coercion', async () => {
|
|
140
|
+
const stringRequest = request({
|
|
141
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: '1', method: 'tools/call', params: {} }),
|
|
142
|
+
})
|
|
143
|
+
const response = jsonResponse({ ...paymentRequired, id: '1' })
|
|
144
|
+
|
|
145
|
+
expect(await getChallengeIds(response, stringRequest)).toEqual([challenge.id])
|
|
146
|
+
expect(await getChallengeIds(jsonResponse(paymentRequired), stringRequest)).toEqual([])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test.each([
|
|
150
|
+
{
|
|
151
|
+
body: '{',
|
|
152
|
+
headers: { accept: 'application/json, text/event-stream' },
|
|
153
|
+
name: 'malformed request JSON',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
157
|
+
headers: { accept: 'application/json, text/event-stream' },
|
|
158
|
+
name: 'request without id',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, params: {} }),
|
|
162
|
+
headers: { accept: 'application/json, text/event-stream' },
|
|
163
|
+
name: 'request without method',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: {} }),
|
|
167
|
+
headers: { accept: 'application/json' },
|
|
168
|
+
name: 'request without Streamable HTTP accept',
|
|
169
|
+
},
|
|
170
|
+
])('does not inspect responses for $name', async ({ body, headers }) => {
|
|
171
|
+
expect(
|
|
172
|
+
await getChallengeIds(jsonResponse(paymentRequired), request({ body, headers })),
|
|
173
|
+
).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('accepts the mcp-method header as MCP request provenance', async () => {
|
|
177
|
+
const requestInit = request({
|
|
178
|
+
headers: { 'mcp-method': 'tools/call' },
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
expect(await getChallengeIds(jsonResponse(paymentRequired), requestInit)).toEqual([
|
|
182
|
+
challenge.id,
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('does not inspect native HTTP 402 responses', async () => {
|
|
187
|
+
expect(await getChallengeIds(jsonResponse(paymentRequired), request({}))).toEqual([
|
|
188
|
+
challenge.id,
|
|
189
|
+
])
|
|
190
|
+
expect(
|
|
191
|
+
await getChallengeIds(
|
|
192
|
+
new Response(JSON.stringify(paymentRequired), {
|
|
193
|
+
status: 402,
|
|
194
|
+
headers: { 'content-type': 'application/json' },
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
197
|
+
).toEqual([])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test.each([
|
|
201
|
+
{ body: sseEvent(paymentRequired), name: 'message event' },
|
|
202
|
+
{ body: sseEvent(paymentRequired, ''), name: 'event without explicit type' },
|
|
203
|
+
{ body: `: keepalive\n\n${sseEvent(paymentRequired)}`, name: 'message after SSE comment' },
|
|
204
|
+
])('extracts payment challenges from SSE $name', async ({ body }) => {
|
|
205
|
+
expect(await getChallengeIds(sseResponse(body))).toEqual([challenge.id])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test.each([
|
|
209
|
+
{ body: sseEvent('{'), name: 'invalid JSON' },
|
|
210
|
+
{ body: sseEvent({ jsonrpc: '2.0', method: 'notifications/progress' }), name: 'notification' },
|
|
211
|
+
{ body: sseEvent(paymentRequired, 'progress'), name: 'non-message event' },
|
|
212
|
+
{
|
|
213
|
+
body:
|
|
214
|
+
sseEvent({ jsonrpc: '2.0', method: 'notifications/progress' }) + sseEvent(paymentRequired),
|
|
215
|
+
name: 'payment challenge after first data event',
|
|
216
|
+
},
|
|
217
|
+
])('ignores SSE $name', async ({ body }) => {
|
|
218
|
+
expect(await getChallengeIds(sseResponse(body))).toEqual([])
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createParser } from 'eventsource-parser'
|
|
2
|
+
|
|
3
|
+
import * as Challenge from '../../../Challenge.js'
|
|
4
|
+
import * as Credential from '../../../Credential.js'
|
|
5
|
+
import * as Mcp from '../../../Mcp.js'
|
|
6
|
+
import type { Protocol } from './Protocol.js'
|
|
7
|
+
import { paymentRequiredStatus } from './Shared.js'
|
|
8
|
+
|
|
9
|
+
/** Returns the JSON-RPC request id — only requests can receive a `-32042` response. */
|
|
10
|
+
function jsonRpcRequestId(body: unknown): number | string | undefined {
|
|
11
|
+
if (typeof body !== 'string') return undefined
|
|
12
|
+
try {
|
|
13
|
+
const message = JSON.parse(body)
|
|
14
|
+
const id = message?.id
|
|
15
|
+
if (message?.jsonrpc !== '2.0' || typeof message?.method !== 'string') return undefined
|
|
16
|
+
return typeof id === 'string' || typeof id === 'number' ? id : undefined
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const responseCache = new WeakMap<Response, Promise<Mcp.Response | undefined>>()
|
|
23
|
+
|
|
24
|
+
function mcpHttpRequestId(request?: RequestInit): number | string | undefined {
|
|
25
|
+
const id = jsonRpcRequestId(request?.body)
|
|
26
|
+
if (id === undefined) return undefined
|
|
27
|
+
const headers = new Headers(request?.headers)
|
|
28
|
+
if (headers.has('mcp-method')) return id
|
|
29
|
+
const accept = headers.get('accept')?.toLowerCase() ?? ''
|
|
30
|
+
return accept.includes('application/json') && accept.includes('text/event-stream')
|
|
31
|
+
? id
|
|
32
|
+
: undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseMessage(value: unknown): Mcp.Response | undefined {
|
|
36
|
+
if (!value || typeof value !== 'object') return undefined
|
|
37
|
+
const message = value as {
|
|
38
|
+
error?: { code?: unknown; message?: unknown }
|
|
39
|
+
id?: unknown
|
|
40
|
+
jsonrpc?: unknown
|
|
41
|
+
result?: unknown
|
|
42
|
+
}
|
|
43
|
+
const id = message.id
|
|
44
|
+
if (message.jsonrpc !== '2.0') return undefined
|
|
45
|
+
if (id !== undefined && typeof id !== 'string' && typeof id !== 'number') return undefined
|
|
46
|
+
const hasError = 'error' in message
|
|
47
|
+
const hasResult = 'result' in message
|
|
48
|
+
if (hasError === hasResult) return undefined
|
|
49
|
+
if (hasError)
|
|
50
|
+
return Number.isInteger(message.error?.code) && typeof message.error?.message === 'string'
|
|
51
|
+
? (value as Mcp.Response)
|
|
52
|
+
: undefined
|
|
53
|
+
return id !== undefined && typeof message.result === 'object'
|
|
54
|
+
? (value as Mcp.Response)
|
|
55
|
+
: undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function paymentRequiredChallenges(
|
|
59
|
+
message: Mcp.Response | undefined,
|
|
60
|
+
id: number | string,
|
|
61
|
+
): Challenge.Challenge[] {
|
|
62
|
+
if (
|
|
63
|
+
!message ||
|
|
64
|
+
message.id !== id ||
|
|
65
|
+
!('error' in message) ||
|
|
66
|
+
message.error?.code !== Mcp.paymentRequiredCode
|
|
67
|
+
)
|
|
68
|
+
return []
|
|
69
|
+
const challenges = message.error?.data?.challenges
|
|
70
|
+
if (!Array.isArray(challenges) || challenges.length === 0) return []
|
|
71
|
+
const parsed: Challenge.Challenge[] = []
|
|
72
|
+
for (const challenge of challenges) {
|
|
73
|
+
const result = Challenge.Schema.safeParse(challenge)
|
|
74
|
+
if (!result.success) return []
|
|
75
|
+
parsed.push(result.data as Challenge.Challenge)
|
|
76
|
+
}
|
|
77
|
+
return parsed
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function parseSseJsonRpcResponse(response: Response): Promise<Mcp.Response | undefined> {
|
|
81
|
+
const reader = response.clone().body?.getReader()
|
|
82
|
+
if (!reader) return undefined
|
|
83
|
+
|
|
84
|
+
const decoder = new TextDecoder()
|
|
85
|
+
let dataEventSeen = false
|
|
86
|
+
let message: Mcp.Response | undefined
|
|
87
|
+
const parser = createParser({
|
|
88
|
+
onEvent(event) {
|
|
89
|
+
if (dataEventSeen || !event.data) return
|
|
90
|
+
dataEventSeen = true
|
|
91
|
+
if (event.event && event.event !== 'message') return
|
|
92
|
+
try {
|
|
93
|
+
message = parseMessage(JSON.parse(event.data))
|
|
94
|
+
} catch {}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
for (;;) {
|
|
100
|
+
if (dataEventSeen) return message
|
|
101
|
+
const { done, value } = await reader.read()
|
|
102
|
+
if (done) {
|
|
103
|
+
parser.feed(decoder.decode())
|
|
104
|
+
parser.reset({ consume: true })
|
|
105
|
+
return message
|
|
106
|
+
}
|
|
107
|
+
parser.feed(decoder.decode(value, { stream: true }))
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
void reader.cancel().catch(() => {})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reads a cloned HTTP body into the first JSON-RPC response message, if any. */
|
|
115
|
+
function parseJsonRpcResponse(response: Response): Promise<Mcp.Response | undefined> {
|
|
116
|
+
const cached = responseCache.get(response)
|
|
117
|
+
if (cached) return cached
|
|
118
|
+
const promise = (async (): Promise<Mcp.Response | undefined> => {
|
|
119
|
+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
|
|
120
|
+
if (contentType.includes('application/json'))
|
|
121
|
+
return response
|
|
122
|
+
.clone()
|
|
123
|
+
.json()
|
|
124
|
+
.then(parseMessage)
|
|
125
|
+
.catch(() => undefined)
|
|
126
|
+
if (contentType.includes('text/event-stream')) return parseSseJsonRpcResponse(response)
|
|
127
|
+
return undefined
|
|
128
|
+
})()
|
|
129
|
+
responseCache.set(response, promise)
|
|
130
|
+
return promise
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* MCP-over-HTTP — remote MCP rides Streamable HTTP, so its challenge is a JSON-RPC `-32042` error
|
|
135
|
+
* in a normal 200 body (often `text/event-stream`), and the credential rides back in `_meta`.
|
|
136
|
+
*/
|
|
137
|
+
export function mcp(): Protocol {
|
|
138
|
+
return {
|
|
139
|
+
getChallenges(response, request) {
|
|
140
|
+
// The 402 schemes own status 402; MCP challenges arrive in a normal 200 body.
|
|
141
|
+
const id = mcpHttpRequestId(request)
|
|
142
|
+
if (response.status === paymentRequiredStatus || id === undefined) return []
|
|
143
|
+
return parseJsonRpcResponse(response).then((message) =>
|
|
144
|
+
paymentRequiredChallenges(message, id),
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
setCredential(request, credential) {
|
|
148
|
+
const message = JSON.parse(request.body as string) as Mcp.Request
|
|
149
|
+
const parsed = Credential.deserialize(credential)
|
|
150
|
+
return {
|
|
151
|
+
...request,
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
...message,
|
|
154
|
+
params: {
|
|
155
|
+
...message.params,
|
|
156
|
+
['_meta']: { ...message.params?.['_meta'], [Mcp.credentialMetaKey]: parsed },
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Challenge from '../../../Challenge.js'
|
|
2
|
+
import * as Constants from '../../../Constants.js'
|
|
3
|
+
import type { Protocol } from './Protocol.js'
|
|
4
|
+
import { paymentRequiredStatus, setCredentialHeader } from './Shared.js'
|
|
5
|
+
|
|
6
|
+
/** MPP — the native HTTP scheme: a 402 carrying a `WWW-Authenticate` challenge, paid back in `Authorization`. */
|
|
7
|
+
export function mpp(): Protocol {
|
|
8
|
+
return {
|
|
9
|
+
getChallenges(response) {
|
|
10
|
+
if (
|
|
11
|
+
response.status !== paymentRequiredStatus ||
|
|
12
|
+
!response.headers.has(Constants.Headers.wwwAuthenticate)
|
|
13
|
+
)
|
|
14
|
+
return []
|
|
15
|
+
return Challenge.fromResponseList(response)
|
|
16
|
+
},
|
|
17
|
+
setCredential(request, credential) {
|
|
18
|
+
return setCredentialHeader(request, Constants.Headers.authorization, credential)
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as Challenge from '../../../Challenge.js'
|
|
2
|
+
import type { MaybePromise } from '../../../internal/types.js'
|
|
3
|
+
|
|
4
|
+
/** One payment protocol over the `http` transport. */
|
|
5
|
+
export type Protocol = {
|
|
6
|
+
/** This protocol's challenges from a response; `[]` when the response isn't its concern. */
|
|
7
|
+
getChallenges: (response: Response, request?: RequestInit) => MaybePromise<Challenge.Challenge[]>
|
|
8
|
+
/** Attaches this protocol's credential to a retry request. */
|
|
9
|
+
setCredential: (request: RequestInit, credential: string) => RequestInit
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as Constants from '../../../Constants.js'
|
|
2
|
+
import * as x402_Types from '../../../x402/Types.js'
|
|
3
|
+
|
|
4
|
+
/** HTTP status that signals a native payment challenge. */
|
|
5
|
+
export const paymentRequiredStatus = 402
|
|
6
|
+
|
|
7
|
+
/** Credential headers purged before a fresh credential is attached. */
|
|
8
|
+
const credentialHeaders = [
|
|
9
|
+
Constants.Headers.authorization,
|
|
10
|
+
x402_Types.paymentRequiredHeader,
|
|
11
|
+
x402_Types.paymentResponseHeader,
|
|
12
|
+
x402_Types.paymentSignatureHeader,
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
/** Attaches `credential` under `header`, clearing any stale credential headers first. */
|
|
16
|
+
export function setCredentialHeader(
|
|
17
|
+
request: RequestInit,
|
|
18
|
+
header: string,
|
|
19
|
+
credential: string,
|
|
20
|
+
): RequestInit {
|
|
21
|
+
const headers = new Headers(request.headers)
|
|
22
|
+
for (const stale of credentialHeaders) headers.delete(stale)
|
|
23
|
+
headers.set(header, credential)
|
|
24
|
+
return { ...request, headers }
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Challenge from '../../../Challenge.js'
|
|
2
|
+
import * as x402_Header from '../../../x402/Header.js'
|
|
3
|
+
import * as x402_ChallengeBrand from '../../../x402/internal/ChallengeBrand.js'
|
|
4
|
+
import * as x402_Types from '../../../x402/Types.js'
|
|
5
|
+
import type { Protocol } from './Protocol.js'
|
|
6
|
+
import { paymentRequiredStatus, setCredentialHeader } from './Shared.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* x402 — a 402 carrying a `PAYMENT-REQUIRED` header, paid back in `PAYMENT-SIGNATURE`. Synthesized
|
|
10
|
+
* challenges are branded for `evm/client/Charge.ts`, keeping them distinct from native `evm`
|
|
11
|
+
* charges with the same method/intent.
|
|
12
|
+
*/
|
|
13
|
+
export function x402(): Protocol {
|
|
14
|
+
return {
|
|
15
|
+
getChallenges(response) {
|
|
16
|
+
if (response.status !== paymentRequiredStatus) return []
|
|
17
|
+
const header = response.headers.get(x402_Types.paymentRequiredHeader)
|
|
18
|
+
if (!header) return []
|
|
19
|
+
const paymentRequired = x402_Header.decodePaymentRequired(header)
|
|
20
|
+
if (response.url && paymentRequired.resource.url !== response.url)
|
|
21
|
+
throw new Error('x402 payment-required resource does not match response URL.')
|
|
22
|
+
return paymentRequired.accepts.map((accepted, index) =>
|
|
23
|
+
x402_ChallengeBrand.mark(
|
|
24
|
+
Challenge.from({
|
|
25
|
+
id: `${x402_Types.syntheticChallengeIdPrefix}${index}`,
|
|
26
|
+
intent: x402_Types.exactIntent,
|
|
27
|
+
method: x402_Types.paymentMethod,
|
|
28
|
+
realm: new URL(paymentRequired.resource.url).host,
|
|
29
|
+
request: {
|
|
30
|
+
...accepted,
|
|
31
|
+
...(paymentRequired.extensions ? { extensions: paymentRequired.extensions } : {}),
|
|
32
|
+
resource: paymentRequired.resource,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
setCredential(request, credential) {
|
|
39
|
+
return setCredentialHeader(request, x402_Types.paymentSignatureHeader, credential)
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -14,7 +14,7 @@ import { Mppx as ServerMppx } from '../server/index.js'
|
|
|
14
14
|
|
|
15
15
|
const account = {} as Account
|
|
16
16
|
const recipient = '0x209693Bc6afc0C5328bA36FaF03C514EF312287C'
|
|
17
|
-
const secretKey = 'test-secret'
|
|
17
|
+
const secretKey = 'test-secret-key-test-secret-key-32'
|
|
18
18
|
const settle = async () => ({
|
|
19
19
|
reference: `0x${'1'.repeat(64)}` as `0x${string}`,
|
|
20
20
|
})
|
package/src/evm/client/index.ts
CHANGED
package/src/evm/index.ts
CHANGED
package/src/evm/server/index.ts
CHANGED
|
@@ -25,7 +25,7 @@ import * as McpServer_transport from '../server/Transport.js'
|
|
|
25
25
|
import * as McpClient from './McpClient.js'
|
|
26
26
|
|
|
27
27
|
const realm = 'api.example.com'
|
|
28
|
-
const secretKey = 'test-secret-key'
|
|
28
|
+
const secretKey = 'test-secret-key-test-secret-key-32'
|
|
29
29
|
const chargeAmountRaw = 1_000_000n
|
|
30
30
|
const doubleSessionAmountRaw = chargeAmountRaw * 2n
|
|
31
31
|
const topUpAmountRaw = chargeAmountRaw * 3n
|
|
@@ -410,6 +410,7 @@ describe.runIf(isLocalnet)('McpClient.wrap integration', () => {
|
|
|
410
410
|
type WrappedClient = {
|
|
411
411
|
callTool: (
|
|
412
412
|
params: { name: string; arguments?: Record<string, unknown>; _meta?: Record<string, unknown> },
|
|
413
|
+
resultSchema?: undefined,
|
|
413
414
|
options?: { context?: unknown; timeout?: number },
|
|
414
415
|
) => Promise<McpClient.CallToolResult>
|
|
415
416
|
}
|
|
@@ -545,9 +546,14 @@ async function createHarness(options?: {
|
|
|
545
546
|
const clientTransport = new StreamableHTTPClientTransport(new URL(`${httpServer.url}/mcp`))
|
|
546
547
|
await sdkClient.connect(clientTransport as never)
|
|
547
548
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
549
|
+
// Wrap a facade over the same session so `sdkClient` keeps raw (challenge-
|
|
550
|
+
// throwing) behavior for the credential-level scenarios below.
|
|
551
|
+
const mcp = McpClient.wrap(
|
|
552
|
+
{ callTool: sdkClient.callTool.bind(sdkClient) as Client['callTool'] },
|
|
553
|
+
{
|
|
554
|
+
methods: [chargeMethod, sessionMethod],
|
|
555
|
+
},
|
|
556
|
+
)
|
|
551
557
|
|
|
552
558
|
return {
|
|
553
559
|
async close() {
|