@civic/x402-mcp 0.0.1

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 (154) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.env.example +10 -0
  3. package/.github/workflows/ci.yml +144 -0
  4. package/.github/workflows/release.yml +56 -0
  5. package/.idea/modules.xml +8 -0
  6. package/.idea/vcs.xml +6 -0
  7. package/.idea/x402-mcp-example.iml +13 -0
  8. package/CLAUDE.md +31 -0
  9. package/DEVELOPER.md +181 -0
  10. package/README.md +274 -0
  11. package/biome.json +63 -0
  12. package/dist/example/client/client.d.ts +3 -0
  13. package/dist/example/client/client.d.ts.map +1 -0
  14. package/dist/example/client/client.js +93 -0
  15. package/dist/example/client/client.js.map +1 -0
  16. package/dist/example/client.d.ts +3 -0
  17. package/dist/example/client.d.ts.map +1 -0
  18. package/dist/example/client.js +105 -0
  19. package/dist/example/client.js.map +1 -0
  20. package/dist/example/config.d.ts +15 -0
  21. package/dist/example/config.d.ts.map +1 -0
  22. package/dist/example/config.js +20 -0
  23. package/dist/example/config.js.map +1 -0
  24. package/dist/example/proxy/client-proxy.d.ts +3 -0
  25. package/dist/example/proxy/client-proxy.d.ts.map +1 -0
  26. package/dist/example/proxy/client-proxy.js +88 -0
  27. package/dist/example/proxy/client-proxy.js.map +1 -0
  28. package/dist/example/proxy/client.d.ts +3 -0
  29. package/dist/example/proxy/client.d.ts.map +1 -0
  30. package/dist/example/proxy/client.js +97 -0
  31. package/dist/example/proxy/client.js.map +1 -0
  32. package/dist/example/proxy/server-proxy.d.ts +3 -0
  33. package/dist/example/proxy/server-proxy.d.ts.map +1 -0
  34. package/dist/example/proxy/server-proxy.js +72 -0
  35. package/dist/example/proxy/server-proxy.js.map +1 -0
  36. package/dist/example/server.d.ts +2 -0
  37. package/dist/example/server.d.ts.map +1 -0
  38. package/dist/example/server.js +76 -0
  39. package/dist/example/server.js.map +1 -0
  40. package/dist/example/service.d.ts +4 -0
  41. package/dist/example/service.d.ts.map +1 -0
  42. package/dist/example/service.js +15 -0
  43. package/dist/example/service.js.map +1 -0
  44. package/dist/scripts/analyzePayment.d.ts +3 -0
  45. package/dist/scripts/analyzePayment.d.ts.map +1 -0
  46. package/dist/scripts/analyzePayment.js +25 -0
  47. package/dist/scripts/analyzePayment.js.map +1 -0
  48. package/dist/scripts/client-proxy.d.ts +3 -0
  49. package/dist/scripts/client-proxy.d.ts.map +1 -0
  50. package/dist/scripts/client-proxy.js +126 -0
  51. package/dist/scripts/client-proxy.js.map +1 -0
  52. package/dist/scripts/generateWallet.d.ts +3 -0
  53. package/dist/scripts/generateWallet.d.ts.map +1 -0
  54. package/dist/scripts/generateWallet.js +15 -0
  55. package/dist/scripts/generateWallet.js.map +1 -0
  56. package/dist/src/client.d.ts +11 -0
  57. package/dist/src/client.d.ts.map +1 -0
  58. package/dist/src/client.js +52 -0
  59. package/dist/src/client.js.map +1 -0
  60. package/dist/src/client.test.d.ts +2 -0
  61. package/dist/src/client.test.d.ts.map +1 -0
  62. package/dist/src/client.test.js +178 -0
  63. package/dist/src/client.test.js.map +1 -0
  64. package/dist/src/index.d.ts +4 -0
  65. package/dist/src/index.d.ts.map +1 -0
  66. package/dist/src/index.js +7 -0
  67. package/dist/src/index.js.map +1 -0
  68. package/dist/src/index.test.d.ts +2 -0
  69. package/dist/src/index.test.d.ts.map +1 -0
  70. package/dist/src/index.test.js +27 -0
  71. package/dist/src/index.test.js.map +1 -0
  72. package/dist/src/mcpClientWithX402.d.ts +14 -0
  73. package/dist/src/mcpClientWithX402.d.ts.map +1 -0
  74. package/dist/src/mcpClientWithX402.js +78 -0
  75. package/dist/src/mcpClientWithX402.js.map +1 -0
  76. package/dist/src/proxy/client.d.ts +23 -0
  77. package/dist/src/proxy/client.d.ts.map +1 -0
  78. package/dist/src/proxy/client.js +39 -0
  79. package/dist/src/proxy/client.js.map +1 -0
  80. package/dist/src/proxy/client.test.d.ts +2 -0
  81. package/dist/src/proxy/client.test.d.ts.map +1 -0
  82. package/dist/src/proxy/client.test.js +167 -0
  83. package/dist/src/proxy/client.test.js.map +1 -0
  84. package/dist/src/proxy/hooks/apiKeyHook.d.ts +22 -0
  85. package/dist/src/proxy/hooks/apiKeyHook.d.ts.map +1 -0
  86. package/dist/src/proxy/hooks/apiKeyHook.js +72 -0
  87. package/dist/src/proxy/hooks/apiKeyHook.js.map +1 -0
  88. package/dist/src/proxy/hooks/apiKeyHook.test.d.ts +2 -0
  89. package/dist/src/proxy/hooks/apiKeyHook.test.d.ts.map +1 -0
  90. package/dist/src/proxy/hooks/apiKeyHook.test.js +240 -0
  91. package/dist/src/proxy/hooks/apiKeyHook.test.js.map +1 -0
  92. package/dist/src/proxy/index.d.ts +4 -0
  93. package/dist/src/proxy/index.d.ts.map +1 -0
  94. package/dist/src/proxy/index.js +4 -0
  95. package/dist/src/proxy/index.js.map +1 -0
  96. package/dist/src/proxy/server.d.ts +18 -0
  97. package/dist/src/proxy/server.d.ts.map +1 -0
  98. package/dist/src/proxy/server.js +35 -0
  99. package/dist/src/proxy/server.js.map +1 -0
  100. package/dist/src/proxy/server.test.d.ts +2 -0
  101. package/dist/src/proxy/server.test.d.ts.map +1 -0
  102. package/dist/src/proxy/server.test.js +26 -0
  103. package/dist/src/proxy/server.test.js.map +1 -0
  104. package/dist/src/server.d.ts +56 -0
  105. package/dist/src/server.d.ts.map +1 -0
  106. package/dist/src/server.js +320 -0
  107. package/dist/src/server.js.map +1 -0
  108. package/dist/src/server.test.d.ts +2 -0
  109. package/dist/src/server.test.d.ts.map +1 -0
  110. package/dist/src/server.test.js +666 -0
  111. package/dist/src/server.test.js.map +1 -0
  112. package/dist/src/util.d.ts +24 -0
  113. package/dist/src/util.d.ts.map +1 -0
  114. package/dist/src/util.js +47 -0
  115. package/dist/src/util.js.map +1 -0
  116. package/dist/src/util.test.d.ts +2 -0
  117. package/dist/src/util.test.d.ts.map +1 -0
  118. package/dist/src/util.test.js +71 -0
  119. package/dist/src/util.test.js.map +1 -0
  120. package/dist/src/x402Transport.d.ts +45 -0
  121. package/dist/src/x402Transport.d.ts.map +1 -0
  122. package/dist/src/x402Transport.js +288 -0
  123. package/dist/src/x402Transport.js.map +1 -0
  124. package/example/client.ts +125 -0
  125. package/example/config.ts +25 -0
  126. package/example/proxy/README.md +95 -0
  127. package/example/proxy/client-proxy.ts +102 -0
  128. package/example/proxy/client.ts +112 -0
  129. package/example/proxy/server-proxy.ts +80 -0
  130. package/example/server.ts +104 -0
  131. package/example/service.ts +19 -0
  132. package/example-client-proxy.sh +3 -0
  133. package/mcp-servers.json +9 -0
  134. package/package.json +53 -0
  135. package/scripts/analyzePayment.ts +25 -0
  136. package/scripts/client-proxy.ts +142 -0
  137. package/scripts/generateWallet.ts +17 -0
  138. package/src/client.test.ts +237 -0
  139. package/src/client.ts +61 -0
  140. package/src/index.test.ts +34 -0
  141. package/src/index.ts +7 -0
  142. package/src/proxy/client.test.ts +199 -0
  143. package/src/proxy/client.ts +61 -0
  144. package/src/proxy/hooks/apiKeyHook.test.ts +276 -0
  145. package/src/proxy/hooks/apiKeyHook.ts +77 -0
  146. package/src/proxy/index.ts +3 -0
  147. package/src/proxy/server.test.ts +33 -0
  148. package/src/proxy/server.ts +43 -0
  149. package/src/server.test.ts +822 -0
  150. package/src/server.ts +451 -0
  151. package/src/util.test.ts +83 -0
  152. package/src/util.ts +48 -0
  153. package/tsconfig.json +20 -0
  154. package/vitest.config.ts +26 -0
@@ -0,0 +1,199 @@
1
+ import { createHttpPassthroughProxy, createStdioPassthroughProxy } from '@civic/passthrough-mcp-server';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import type { Wallet } from 'x402/types';
4
+ import { makePaymentAwareClientTransport } from '../client.js';
5
+ import { createClientProxy } from './client.js';
6
+
7
+ // Mock dependencies
8
+ vi.mock('@civic/passthrough-mcp-server', () => ({
9
+ createHttpPassthroughProxy: vi.fn(),
10
+ createStdioPassthroughProxy: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../client.js', () => ({
14
+ makePaymentAwareClientTransport: vi.fn(),
15
+ }));
16
+
17
+ describe('createClientProxy', () => {
18
+ let mockWallet: Wallet;
19
+ let mockHttpProxy: any;
20
+ let mockStdioProxy: any;
21
+ let mockTransport: any;
22
+
23
+ beforeEach(async () => {
24
+ vi.clearAllMocks();
25
+
26
+ mockWallet = {
27
+ account: { address: '0xabc' },
28
+ chain: { id: 1 },
29
+ } as unknown as Wallet;
30
+
31
+ mockHttpProxy = {
32
+ start: vi.fn().mockResolvedValue(undefined),
33
+ stop: vi.fn().mockResolvedValue(undefined),
34
+ };
35
+
36
+ mockStdioProxy = {
37
+ start: vi.fn().mockResolvedValue(undefined),
38
+ stop: vi.fn().mockResolvedValue(undefined),
39
+ };
40
+
41
+ mockTransport = {
42
+ send: vi.fn(),
43
+ receive: vi.fn(),
44
+ };
45
+
46
+ vi.mocked(makePaymentAwareClientTransport).mockReturnValue(mockTransport);
47
+ vi.mocked(createHttpPassthroughProxy).mockResolvedValue(mockHttpProxy);
48
+ vi.mocked(createStdioPassthroughProxy).mockResolvedValue(mockStdioProxy);
49
+ });
50
+
51
+ afterEach(() => {
52
+ vi.restoreAllMocks();
53
+ });
54
+
55
+ describe('HTTP mode', () => {
56
+ it('should create an HTTP proxy with default port', async () => {
57
+ const targetUrl = 'http://example.com/mcp';
58
+
59
+ const proxy = await createClientProxy({
60
+ targetUrl,
61
+ wallet: mockWallet,
62
+ mode: 'http',
63
+ });
64
+
65
+ expect(makePaymentAwareClientTransport).toHaveBeenCalledWith(targetUrl, mockWallet);
66
+
67
+ expect(vi.mocked(createHttpPassthroughProxy)).toHaveBeenCalledWith({
68
+ port: 4000,
69
+ target: {
70
+ transportType: 'custom',
71
+ transportFactory: expect.any(Function),
72
+ },
73
+ });
74
+
75
+ expect(proxy).toBe(mockHttpProxy);
76
+ });
77
+
78
+ it('should create an HTTP proxy with custom port', async () => {
79
+ const targetUrl = 'http://example.com/mcp';
80
+ const customPort = 5555;
81
+
82
+ const proxy = await createClientProxy({
83
+ targetUrl,
84
+ wallet: mockWallet,
85
+ mode: 'http',
86
+ port: customPort,
87
+ });
88
+
89
+ expect(vi.mocked(createHttpPassthroughProxy)).toHaveBeenCalledWith({
90
+ port: customPort,
91
+ target: {
92
+ transportType: 'custom',
93
+ transportFactory: expect.any(Function),
94
+ },
95
+ });
96
+
97
+ expect(proxy).toBe(mockHttpProxy);
98
+ });
99
+
100
+ it('should use the transport factory correctly', async () => {
101
+ const targetUrl = 'http://example.com/mcp';
102
+
103
+ await createClientProxy({
104
+ targetUrl,
105
+ wallet: mockWallet,
106
+ mode: 'http',
107
+ });
108
+
109
+ const callArgs = vi.mocked(createHttpPassthroughProxy).mock.calls[0][0];
110
+
111
+ // Check that target is configured with custom transport
112
+ expect(callArgs.target.transportType).toBe('custom');
113
+
114
+ // Test that the factory returns the payment-aware transport
115
+ if (callArgs.target.transportType === 'custom' && 'transportFactory' in callArgs.target) {
116
+ const transport = callArgs.target.transportFactory();
117
+ expect(transport).toBe(mockTransport);
118
+ }
119
+ });
120
+ });
121
+
122
+ describe('stdio mode', () => {
123
+ it('should create a stdio proxy', async () => {
124
+ const targetUrl = 'http://example.com/mcp';
125
+
126
+ const proxy = await createClientProxy({
127
+ targetUrl,
128
+ wallet: mockWallet,
129
+ mode: 'stdio',
130
+ });
131
+
132
+ expect(makePaymentAwareClientTransport).toHaveBeenCalledWith(targetUrl, mockWallet);
133
+
134
+ expect(vi.mocked(createStdioPassthroughProxy)).toHaveBeenCalledWith({
135
+ target: {
136
+ transportType: 'custom',
137
+ transportFactory: expect.any(Function),
138
+ },
139
+ });
140
+
141
+ expect(proxy).toBe(mockStdioProxy);
142
+ });
143
+
144
+ it('should use the transport factory correctly in stdio mode', async () => {
145
+ const targetUrl = 'http://example.com/mcp';
146
+
147
+ await createClientProxy({
148
+ targetUrl,
149
+ wallet: mockWallet,
150
+ mode: 'stdio',
151
+ });
152
+
153
+ const callArgs = vi.mocked(createStdioPassthroughProxy).mock.calls[0][0];
154
+
155
+ // Check that target is configured with custom transport
156
+ expect(callArgs.target.transportType).toBe('custom');
157
+
158
+ // Test that the factory returns the payment-aware transport
159
+ if (callArgs.target.transportType === 'custom' && 'transportFactory' in callArgs.target) {
160
+ const transport = callArgs.target.transportFactory();
161
+ expect(transport).toBe(mockTransport);
162
+ }
163
+ });
164
+ });
165
+
166
+ describe('transport integration', () => {
167
+ it('should pass wallet to payment-aware transport', async () => {
168
+ const targetUrl = 'https://secure.example.com/mcp';
169
+ const wallet = {
170
+ account: { address: '0x123456' },
171
+ chain: { id: 8453 },
172
+ } as unknown as Wallet;
173
+
174
+ await createClientProxy({
175
+ targetUrl,
176
+ wallet,
177
+ mode: 'http',
178
+ });
179
+
180
+ expect(makePaymentAwareClientTransport).toHaveBeenCalledWith(targetUrl, wallet);
181
+ });
182
+
183
+ it('should handle different URL formats', async () => {
184
+ const urls = ['http://localhost:3000/mcp', 'https://api.example.com/v1/mcp', 'http://192.168.1.1:8080'];
185
+
186
+ for (const url of urls) {
187
+ vi.clearAllMocks();
188
+
189
+ await createClientProxy({
190
+ targetUrl: url,
191
+ wallet: mockWallet,
192
+ mode: 'stdio',
193
+ });
194
+
195
+ expect(makePaymentAwareClientTransport).toHaveBeenCalledWith(url, mockWallet);
196
+ }
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,61 @@
1
+ import {
2
+ createHttpPassthroughProxy,
3
+ createStdioPassthroughProxy,
4
+ type HttpProxyConfig,
5
+ type PassthroughProxy,
6
+ type StdioProxyConfig,
7
+ } from '@civic/passthrough-mcp-server';
8
+ import type { Wallet } from 'x402/types';
9
+ import { makePaymentAwareClientTransport } from '../client.js';
10
+
11
+ /**
12
+ * Creates a client-side proxy that handles x402 payments on behalf of MCP clients
13
+ * that don't support payment protocols.
14
+ *
15
+ * @param params - Configuration for the proxy
16
+ * @param params.targetUrl - The URL of the x402-enabled MCP server to proxy to
17
+ * @param params.walletClient - A viem WalletClient configured with account and chain for payments
18
+ * @param params.mode - 'stdio' for stdio transport or 'http' for HTTP transport
19
+ * @param params.port - The port for the proxy to listen on (only for HTTP mode, default: 4000)
20
+ * @returns The proxy instance
21
+ */
22
+ export async function createClientProxy(
23
+ params: {
24
+ targetUrl: string;
25
+ wallet: Wallet;
26
+ } & (
27
+ | {
28
+ mode: 'stdio';
29
+ }
30
+ | {
31
+ mode: 'http';
32
+ port?: number;
33
+ }
34
+ )
35
+ ): Promise<PassthroughProxy> {
36
+ // Create the payment-aware transport for the target
37
+ const paymentAwareTransport = makePaymentAwareClientTransport(params.targetUrl, params.wallet);
38
+
39
+ if (params.mode === 'stdio') {
40
+ // Create stdio proxy configuration
41
+ const config: StdioProxyConfig = {
42
+ target: {
43
+ transportType: 'custom',
44
+ transportFactory: () => paymentAwareTransport,
45
+ },
46
+ };
47
+
48
+ return createStdioPassthroughProxy(config);
49
+ } else {
50
+ // Create HTTP proxy configuration
51
+ const config: HttpProxyConfig = {
52
+ port: params.port ?? 4000,
53
+ target: {
54
+ transportType: 'custom',
55
+ transportFactory: () => paymentAwareTransport,
56
+ },
57
+ };
58
+
59
+ return createHttpPassthroughProxy(config);
60
+ }
61
+ }
@@ -0,0 +1,276 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ApiKeyHook } from './apiKeyHook.js';
3
+
4
+ describe('ApiKeyHook', () => {
5
+ let hook: ApiKeyHook;
6
+ const testApiKey = 'sk-test-123456789';
7
+
8
+ beforeEach(() => {
9
+ hook = new ApiKeyHook(testApiKey);
10
+ });
11
+
12
+ afterEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe('constructor and name', () => {
17
+ it('should initialize with provided API key', () => {
18
+ expect(hook).toBeDefined();
19
+ expect(hook).toBeInstanceOf(ApiKeyHook);
20
+ });
21
+
22
+ it('should have correct name', () => {
23
+ expect(hook.name).toBe('api-key-injector');
24
+ });
25
+ });
26
+
27
+ describe('processToolCallRequest', () => {
28
+ it('should add Authorization header to request', async () => {
29
+ const request = {
30
+ jsonrpc: '2.0',
31
+ method: 'tools/call',
32
+ params: { name: 'test-tool' },
33
+ id: 1,
34
+ };
35
+
36
+ const result = await hook.processToolCallRequest(request);
37
+
38
+ expect(result).toEqual({
39
+ resultType: 'continue',
40
+ request: {
41
+ ...request,
42
+ requestContext: {
43
+ headers: {
44
+ Authorization: `Bearer ${testApiKey}`,
45
+ },
46
+ },
47
+ },
48
+ });
49
+ });
50
+
51
+ it('should preserve existing requestContext', async () => {
52
+ const request = {
53
+ jsonrpc: '2.0',
54
+ method: 'tools/call',
55
+ params: { name: 'test-tool' },
56
+ id: 1,
57
+ requestContext: {
58
+ existingField: 'value',
59
+ headers: {
60
+ 'X-Custom-Header': 'custom-value',
61
+ },
62
+ },
63
+ };
64
+
65
+ const result = await hook.processToolCallRequest(request);
66
+
67
+ expect(result).toEqual({
68
+ resultType: 'continue',
69
+ request: {
70
+ ...request,
71
+ requestContext: {
72
+ existingField: 'value',
73
+ headers: {
74
+ 'X-Custom-Header': 'custom-value',
75
+ Authorization: `Bearer ${testApiKey}`,
76
+ },
77
+ },
78
+ },
79
+ });
80
+ });
81
+
82
+ it('should override existing Authorization header', async () => {
83
+ const request = {
84
+ jsonrpc: '2.0',
85
+ method: 'tools/call',
86
+ params: {},
87
+ id: 3,
88
+ requestContext: {
89
+ headers: {
90
+ Authorization: 'Bearer old-key',
91
+ },
92
+ },
93
+ };
94
+
95
+ const result = await hook.processToolCallRequest(request);
96
+
97
+ expect(result.request.requestContext.headers.Authorization).toBe(`Bearer ${testApiKey}`);
98
+ });
99
+
100
+ it('should handle request without requestContext', async () => {
101
+ const request = {
102
+ jsonrpc: '2.0',
103
+ method: 'tools/call',
104
+ params: {},
105
+ id: 4,
106
+ };
107
+
108
+ const result = await hook.processToolCallRequest(request);
109
+
110
+ expect(result).toEqual({
111
+ resultType: 'continue',
112
+ request: {
113
+ ...request,
114
+ requestContext: {
115
+ headers: {
116
+ Authorization: `Bearer ${testApiKey}`,
117
+ },
118
+ },
119
+ },
120
+ });
121
+ });
122
+ });
123
+
124
+ describe('processToolsListRequest', () => {
125
+ it('should add Authorization header to tools list request', async () => {
126
+ const request = {
127
+ jsonrpc: '2.0',
128
+ method: 'tools/list',
129
+ params: {},
130
+ id: 1,
131
+ };
132
+
133
+ const result = await hook.processToolsListRequest(request);
134
+
135
+ expect(result).toEqual({
136
+ resultType: 'continue',
137
+ request: {
138
+ ...request,
139
+ requestContext: {
140
+ headers: {
141
+ Authorization: `Bearer ${testApiKey}`,
142
+ },
143
+ },
144
+ },
145
+ });
146
+ });
147
+
148
+ it('should preserve existing headers in tools list', async () => {
149
+ const request = {
150
+ jsonrpc: '2.0',
151
+ method: 'tools/list',
152
+ params: {},
153
+ id: 2,
154
+ requestContext: {
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ },
159
+ };
160
+
161
+ const result = await hook.processToolsListRequest(request);
162
+
163
+ expect(result.request.requestContext.headers).toEqual({
164
+ 'Content-Type': 'application/json',
165
+ Authorization: `Bearer ${testApiKey}`,
166
+ });
167
+ });
168
+ });
169
+
170
+ describe('processInitializeRequest', () => {
171
+ it('should add Authorization header to initialize request', async () => {
172
+ const request = {
173
+ jsonrpc: '2.0',
174
+ method: 'initialize',
175
+ params: {
176
+ clientInfo: {
177
+ name: 'test-client',
178
+ version: '1.0.0',
179
+ },
180
+ },
181
+ id: 1,
182
+ };
183
+
184
+ const result = await hook.processInitializeRequest(request);
185
+
186
+ expect(result).toEqual({
187
+ resultType: 'continue',
188
+ request: {
189
+ ...request,
190
+ requestContext: {
191
+ headers: {
192
+ Authorization: `Bearer ${testApiKey}`,
193
+ },
194
+ },
195
+ },
196
+ });
197
+ });
198
+
199
+ it('should preserve existing requestContext in initialize', async () => {
200
+ const request = {
201
+ jsonrpc: '2.0',
202
+ method: 'initialize',
203
+ params: {},
204
+ id: 2,
205
+ requestContext: {
206
+ sessionId: 'session-123',
207
+ headers: {
208
+ 'User-Agent': 'TestClient/1.0',
209
+ },
210
+ },
211
+ };
212
+
213
+ const result = await hook.processInitializeRequest(request);
214
+
215
+ expect(result).toEqual({
216
+ resultType: 'continue',
217
+ request: {
218
+ ...request,
219
+ requestContext: {
220
+ sessionId: 'session-123',
221
+ headers: {
222
+ 'User-Agent': 'TestClient/1.0',
223
+ Authorization: `Bearer ${testApiKey}`,
224
+ },
225
+ },
226
+ },
227
+ });
228
+ });
229
+ });
230
+
231
+ describe('different API key formats', () => {
232
+ it('should work with various API key formats', async () => {
233
+ const apiKeys = [
234
+ 'simple-key',
235
+ 'sk-proj-very-long-api-key-with-many-characters',
236
+ 'Bearer existing-token',
237
+ 'ApiKey 12345',
238
+ ];
239
+
240
+ for (const apiKey of apiKeys) {
241
+ const customHook = new ApiKeyHook(apiKey);
242
+ const request = {
243
+ jsonrpc: '2.0',
244
+ method: 'tools/call',
245
+ params: {},
246
+ id: 1,
247
+ };
248
+
249
+ const result = await customHook.processToolCallRequest(request);
250
+
251
+ expect(result.request.requestContext.headers.Authorization).toBe(`Bearer ${apiKey}`);
252
+ }
253
+ });
254
+ });
255
+
256
+ describe('all methods add the same header', () => {
257
+ it('should add consistent headers across all methods', async () => {
258
+ const request = {
259
+ jsonrpc: '2.0',
260
+ method: 'test',
261
+ params: {},
262
+ id: 1,
263
+ };
264
+
265
+ const toolCallResult = await hook.processToolCallRequest(request);
266
+ const toolsListResult = await hook.processToolsListRequest(request);
267
+ const initializeResult = await hook.processInitializeRequest(request);
268
+
269
+ const expectedHeader = `Bearer ${testApiKey}`;
270
+
271
+ expect(toolCallResult.request.requestContext.headers.Authorization).toBe(expectedHeader);
272
+ expect(toolsListResult.request.requestContext.headers.Authorization).toBe(expectedHeader);
273
+ expect(initializeResult.request.requestContext.headers.Authorization).toBe(expectedHeader);
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,77 @@
1
+ import { AbstractHook } from '@civic/passthrough-mcp-server';
2
+
3
+ /**
4
+ * Hook that adds an API key to outgoing requests to authenticate with upstream MCP servers
5
+ */
6
+ export class ApiKeyHook extends AbstractHook {
7
+ constructor(private apiKey: string) {
8
+ super();
9
+ }
10
+
11
+ get name(): string {
12
+ return 'api-key-injector';
13
+ }
14
+
15
+ /**
16
+ * Add API key to tool call requests
17
+ */
18
+ async processToolCallRequest(request: any): Promise<any> {
19
+ const modifiedRequest = {
20
+ ...request,
21
+ requestContext: {
22
+ ...request.requestContext,
23
+ headers: {
24
+ ...request.requestContext?.headers,
25
+ Authorization: `Bearer ${this.apiKey}`,
26
+ },
27
+ },
28
+ };
29
+
30
+ return {
31
+ resultType: 'continue',
32
+ request: modifiedRequest,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Add API key to tools list requests
38
+ */
39
+ async processToolsListRequest(request: any): Promise<any> {
40
+ const modifiedRequest = {
41
+ ...request,
42
+ requestContext: {
43
+ ...request.requestContext,
44
+ headers: {
45
+ ...request.requestContext?.headers,
46
+ Authorization: `Bearer ${this.apiKey}`,
47
+ },
48
+ },
49
+ };
50
+
51
+ return {
52
+ resultType: 'continue',
53
+ request: modifiedRequest,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Add API key to initialize requests
59
+ */
60
+ async processInitializeRequest(request: any): Promise<any> {
61
+ const modifiedRequest = {
62
+ ...request,
63
+ requestContext: {
64
+ ...request.requestContext,
65
+ headers: {
66
+ ...request.requestContext?.headers,
67
+ Authorization: `Bearer ${this.apiKey}`,
68
+ },
69
+ },
70
+ };
71
+
72
+ return {
73
+ resultType: 'continue',
74
+ request: modifiedRequest,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,3 @@
1
+ export { createClientProxy } from './client.js';
2
+ export { ApiKeyHook } from './hooks/apiKeyHook.js';
3
+ export { createServerProxy } from './server.js';
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ // Test the ApiKeyHook separately since it's the part we can easily test
4
+ describe('ApiKeyHook integration', () => {
5
+ it('should create ApiKeyHook with correct properties', async () => {
6
+ const { ApiKeyHook } = await import('./hooks/apiKeyHook.js');
7
+
8
+ const apiKey = 'sk-test-123456';
9
+ const hook = new ApiKeyHook(apiKey);
10
+
11
+ expect(hook).toBeDefined();
12
+ expect(hook.name).toBe('api-key-injector');
13
+ });
14
+
15
+ it('should add Authorization header in processToolCallRequest', async () => {
16
+ const { ApiKeyHook } = await import('./hooks/apiKeyHook.js');
17
+
18
+ const apiKey = 'sk-test-789';
19
+ const hook = new ApiKeyHook(apiKey);
20
+
21
+ const request = {
22
+ jsonrpc: '2.0',
23
+ method: 'tools/call',
24
+ params: {},
25
+ id: 1,
26
+ };
27
+
28
+ const result = await hook.processToolCallRequest(request);
29
+
30
+ expect(result.resultType).toBe('continue');
31
+ expect(result.request.requestContext.headers.Authorization).toBe(`Bearer ${apiKey}`);
32
+ });
33
+ });
@@ -0,0 +1,43 @@
1
+ import { createPassthroughProxy, type PassthroughProxy } from '@civic/passthrough-mcp-server';
2
+ import type { Address } from 'viem';
3
+ import { makePaymentAwareServerTransport } from '../server.js';
4
+ import { ApiKeyHook } from './hooks/apiKeyHook.js';
5
+
6
+ /**
7
+ * Creates a server-side proxy that:
8
+ * 1. Accepts x402 payments from clients
9
+ * 2. Adds an API key to authenticate with the upstream MCP server
10
+ *
11
+ * This allows monetizing access to API-key-protected MCP servers via micropayments.
12
+ *
13
+ * @param upstreamUrl - The URL of the API-key-protected MCP server to proxy to
14
+ * @param apiKey - The API key to authenticate with the upstream server
15
+ * @param paymentWallet - The wallet address to receive payments
16
+ * @param toolPricing - Mapping of tool names to prices (e.g., { "my-tool": "$0.01" })
17
+ * @param port - The port for the proxy to listen on (default: 5000)
18
+ * @returns The proxy instance
19
+ */
20
+ export async function createServerProxy(
21
+ upstreamUrl: string,
22
+ apiKey: string,
23
+ paymentWallet: Address | string,
24
+ toolPricing: Record<string, string>
25
+ ): Promise<PassthroughProxy> {
26
+ // Create the payment-aware server transport
27
+ const paymentAwareTransport = makePaymentAwareServerTransport(paymentWallet, toolPricing);
28
+
29
+ // Create the API key hook
30
+ const apiKeyHook = new ApiKeyHook(apiKey);
31
+
32
+ // Create and return the proxy
33
+ return createPassthroughProxy({
34
+ sourceTransportType: 'custom',
35
+ sourceTransport: paymentAwareTransport,
36
+ target: {
37
+ url: upstreamUrl,
38
+ transportType: 'httpStream',
39
+ },
40
+ hooks: [apiKeyHook],
41
+ autoStart: true,
42
+ });
43
+ }