@gemus/mcp-proxy 0.1.9 → 0.1.10
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/README.md +41 -21
- package/package.json +3 -2
- package/src/proxy.mjs +29 -427
- package/src/relay.mjs +440 -0
- package/src/setup/cli.mjs +130 -0
- package/src/setup/codex.mjs +110 -0
- package/src/setup/config.mjs +530 -0
- package/src/setup/configFile.mjs +155 -0
- package/src/setup/environment.mjs +79 -0
- package/src/setup/secret.mjs +91 -0
- package/src/__tests__/backfill.test.ts +0 -394
- package/src/__tests__/failFast.test.ts +0 -356
- package/src/__tests__/fixtures/proxy-runtime-loader.mjs +0 -216
- package/src/__tests__/mcpHeaders.test.ts +0 -32
- package/src/__tests__/proxyMessages.test.ts +0 -81
- package/src/__tests__/route.integration.test.ts +0 -68
- package/src/__tests__/startup.test.ts +0 -752
- package/src/__tests__/upstreamHttp.real.test.ts +0 -37
- package/src/__tests__/upstreamHttp.test.ts +0 -151
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { createServer, type Server } from 'node:http'
|
|
2
|
-
import type { Socket } from 'node:net'
|
|
3
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
-
import { createUpstreamHttp } from '../upstreamHttp.mjs'
|
|
5
|
-
|
|
6
|
-
let server: Server | undefined
|
|
7
|
-
|
|
8
|
-
afterEach(async () => {
|
|
9
|
-
server?.closeAllConnections()
|
|
10
|
-
if (server?.listening) {
|
|
11
|
-
await new Promise<void>((resolve) => server?.close(() => resolve()))
|
|
12
|
-
}
|
|
13
|
-
server = undefined
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('createUpstreamHttp with real Undici dispatchers', () => {
|
|
17
|
-
it('force-closes an established hanging response after the graceful budget expires', async () => {
|
|
18
|
-
let activeSocket!: Socket
|
|
19
|
-
server = createServer((request, response) => {
|
|
20
|
-
activeSocket = request.socket
|
|
21
|
-
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
|
22
|
-
response.write(': heartbeat\n\n')
|
|
23
|
-
})
|
|
24
|
-
await new Promise<void>((resolve) => server?.listen(0, '127.0.0.1', resolve))
|
|
25
|
-
const address = server.address()
|
|
26
|
-
if (!address || typeof address === 'string') throw new Error('expected a TCP listener')
|
|
27
|
-
|
|
28
|
-
const upstream = createUpstreamHttp()
|
|
29
|
-
const response = await upstream.fetch(`http://127.0.0.1:${address.port}/api/mcp`)
|
|
30
|
-
expect(response.ok).toBe(true)
|
|
31
|
-
expect(activeSocket.destroyed).toBe(false)
|
|
32
|
-
|
|
33
|
-
await upstream.shutdown()
|
|
34
|
-
|
|
35
|
-
await expect.poll(() => activeSocket.destroyed, { timeout: 500 }).toBe(true)
|
|
36
|
-
}, 5000)
|
|
37
|
-
})
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
-
|
|
3
|
-
const undiciFetch = vi.fn()
|
|
4
|
-
const agents: Array<{ options: unknown; close: ReturnType<typeof vi.fn>; destroy: ReturnType<typeof vi.fn> }> = []
|
|
5
|
-
const pools: Array<{ origin: unknown; options: unknown; destroy: ReturnType<typeof vi.fn> }> = []
|
|
6
|
-
|
|
7
|
-
const Agent = vi.fn(function Agent(options) {
|
|
8
|
-
const agent = {
|
|
9
|
-
options,
|
|
10
|
-
close: vi.fn(() => Promise.resolve()),
|
|
11
|
-
destroy: vi.fn(),
|
|
12
|
-
}
|
|
13
|
-
agents.push(agent)
|
|
14
|
-
return agent
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const Pool = vi.fn(function Pool(origin, options) {
|
|
18
|
-
const pool = {
|
|
19
|
-
origin,
|
|
20
|
-
options,
|
|
21
|
-
destroy: vi.fn(() => Promise.resolve()),
|
|
22
|
-
}
|
|
23
|
-
pools.push(pool)
|
|
24
|
-
return pool
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
vi.mock('undici', () => ({ Agent, Pool, fetch: undiciFetch }))
|
|
28
|
-
|
|
29
|
-
async function createAdapter() {
|
|
30
|
-
return (await import('../upstreamHttp.mjs')).createUpstreamHttp()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
agents.length = 0
|
|
35
|
-
pools.length = 0
|
|
36
|
-
Agent.mockClear()
|
|
37
|
-
Pool.mockClear()
|
|
38
|
-
undiciFetch.mockReset()
|
|
39
|
-
delete process.env.PROXY_CONNECT_ATTEMPT_TIMEOUT_MS
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
vi.useRealTimers()
|
|
44
|
-
delete process.env.PROXY_CONNECT_ATTEMPT_TIMEOUT_MS
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('createUpstreamHttp', () => {
|
|
48
|
-
it('creates one Agent with the 1000ms address-family attempt window by default', async () => {
|
|
49
|
-
const upstream = await createAdapter()
|
|
50
|
-
|
|
51
|
-
expect(upstream.attemptTimeoutMs).toBe(1000)
|
|
52
|
-
expect(Agent).toHaveBeenCalledTimes(1)
|
|
53
|
-
expect(agents[0].options).toEqual({
|
|
54
|
-
factory: expect.any(Function),
|
|
55
|
-
connect: {
|
|
56
|
-
autoSelectFamily: true,
|
|
57
|
-
autoSelectFamilyAttemptTimeout: 1000,
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it.each([
|
|
63
|
-
['', 1000],
|
|
64
|
-
['not-a-number', 1000],
|
|
65
|
-
['Infinity', 1000],
|
|
66
|
-
['0', 1000],
|
|
67
|
-
['-1', 1000],
|
|
68
|
-
['9.9', 10],
|
|
69
|
-
['10.9', 10],
|
|
70
|
-
['251.9', 251],
|
|
71
|
-
])('normalizes PROXY_CONNECT_ATTEMPT_TIMEOUT_MS=%j to %i milliseconds', async (value, expected) => {
|
|
72
|
-
process.env.PROXY_CONNECT_ATTEMPT_TIMEOUT_MS = value
|
|
73
|
-
|
|
74
|
-
const upstream = await createAdapter()
|
|
75
|
-
|
|
76
|
-
expect(upstream.attemptTimeoutMs).toBe(expected)
|
|
77
|
-
expect(agents[0].options).toEqual({
|
|
78
|
-
factory: expect.any(Function),
|
|
79
|
-
connect: {
|
|
80
|
-
autoSelectFamily: true,
|
|
81
|
-
autoSelectFamilyAttemptTimeout: expected,
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('preserves transport request init while injecting its dispatcher', async () => {
|
|
87
|
-
const upstream = await createAdapter()
|
|
88
|
-
const signal = new AbortController().signal
|
|
89
|
-
const init = {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
headers: { authorization: 'Bearer example', accept: 'text/event-stream' },
|
|
92
|
-
body: '{"jsonrpc":"2.0","id":7}',
|
|
93
|
-
signal,
|
|
94
|
-
cache: 'no-store' as RequestCache,
|
|
95
|
-
redirect: 'manual' as RequestRedirect,
|
|
96
|
-
}
|
|
97
|
-
undiciFetch.mockResolvedValue(new Response('ok'))
|
|
98
|
-
|
|
99
|
-
await upstream.fetch('https://example.test/api/mcp', init)
|
|
100
|
-
|
|
101
|
-
expect(undiciFetch).toHaveBeenCalledTimes(1)
|
|
102
|
-
expect(undiciFetch).toHaveBeenCalledWith('https://example.test/api/mcp', {
|
|
103
|
-
...init,
|
|
104
|
-
dispatcher: agents[0],
|
|
105
|
-
})
|
|
106
|
-
expect(init).not.toHaveProperty('dispatcher')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('propagates a failed request after one fetch attempt', async () => {
|
|
110
|
-
const upstream = await createAdapter()
|
|
111
|
-
undiciFetch.mockRejectedValue(new Error('connect failed'))
|
|
112
|
-
|
|
113
|
-
await expect(upstream.fetch('https://example.test/api/mcp', { method: 'POST', body: 'side-effecting' })).rejects.toThrow('connect failed')
|
|
114
|
-
|
|
115
|
-
expect(undiciFetch).toHaveBeenCalledTimes(1)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('shares one graceful shutdown across repeated calls', async () => {
|
|
119
|
-
const upstream = await createAdapter()
|
|
120
|
-
let finishClose!: () => void
|
|
121
|
-
agents[0].close.mockImplementation(() => new Promise<void>((resolve) => { finishClose = resolve }))
|
|
122
|
-
|
|
123
|
-
const first = upstream.shutdown()
|
|
124
|
-
const second = upstream.shutdown()
|
|
125
|
-
expect(second).toBe(first)
|
|
126
|
-
expect(agents[0].close).toHaveBeenCalledTimes(1)
|
|
127
|
-
|
|
128
|
-
finishClose()
|
|
129
|
-
await first
|
|
130
|
-
await upstream.shutdown()
|
|
131
|
-
expect(agents[0].close).toHaveBeenCalledTimes(1)
|
|
132
|
-
expect(agents[0].destroy).not.toHaveBeenCalled()
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('destroys retained origin dispatchers and the Agent when graceful close exceeds 2000ms', async () => {
|
|
136
|
-
vi.useFakeTimers()
|
|
137
|
-
const upstream = await createAdapter()
|
|
138
|
-
agents[0].close.mockImplementation(() => new Promise<void>(() => {}))
|
|
139
|
-
const factory = (agents[0].options as { factory: (origin: string, options: object) => unknown }).factory
|
|
140
|
-
factory('https://example.test', { connections: 2 })
|
|
141
|
-
|
|
142
|
-
const shutdown = upstream.shutdown()
|
|
143
|
-
await vi.advanceTimersByTimeAsync(2000)
|
|
144
|
-
await shutdown
|
|
145
|
-
|
|
146
|
-
expect(agents[0].close).toHaveBeenCalledTimes(1)
|
|
147
|
-
expect(agents[0].destroy).toHaveBeenCalledTimes(1)
|
|
148
|
-
expect(pools[0].destroy).toHaveBeenCalledTimes(1)
|
|
149
|
-
expect(upstream.shutdown()).toBe(shutdown)
|
|
150
|
-
})
|
|
151
|
-
})
|