@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.
@@ -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
- })