cprime-supergateway 3.4.3
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/.github/workflows/docker-publish.yaml +79 -0
- package/.husky/pre-commit +17 -0
- package/.prettierignore +8 -0
- package/.prettierrc +5 -0
- package/AGENTS.md +29 -0
- package/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/gateways/sseToStdio.js +139 -0
- package/dist/gateways/stdioToSse.js +147 -0
- package/dist/gateways/stdioToStatefulStreamableHttp.js +188 -0
- package/dist/gateways/stdioToStatelessStreamableHttp.js +208 -0
- package/dist/gateways/stdioToWs.js +113 -0
- package/dist/gateways/streamableHttpToStdio.js +134 -0
- package/dist/index.js +266 -0
- package/dist/lib/corsOrigin.js +23 -0
- package/dist/lib/getLogger.js +44 -0
- package/dist/lib/getVersion.js +16 -0
- package/dist/lib/headers.js +31 -0
- package/dist/lib/onSignals.js +27 -0
- package/dist/lib/serializeCorsOrigin.js +6 -0
- package/dist/lib/sessionAccessCounter.js +77 -0
- package/dist/server/websocket.js +102 -0
- package/dist/services/EncryptionService.js +236 -0
- package/dist/types.js +1 -0
- package/docker/base.Dockerfile +9 -0
- package/docker/deno.Dockerfile +2 -0
- package/docker/uvx.Dockerfile +3 -0
- package/docker-bake.hcl +51 -0
- package/package.json +61 -0
- package/scripts/decrypt-sample.ts +34 -0
- package/scripts/encryption-play.ts +145 -0
- package/src/gateways/sseToStdio.ts +195 -0
- package/src/gateways/stdioToSse.ts +260 -0
- package/src/gateways/stdioToStatefulStreamableHttp.ts +274 -0
- package/src/gateways/stdioToStatelessStreamableHttp.ts +303 -0
- package/src/gateways/stdioToWs.ts +151 -0
- package/src/gateways/streamableHttpToStdio.ts +196 -0
- package/src/index.ts +286 -0
- package/src/lib/corsOrigin.ts +31 -0
- package/src/lib/getLogger.ts +83 -0
- package/src/lib/getVersion.ts +17 -0
- package/src/lib/headers.ts +55 -0
- package/src/lib/initMongoClient.ts +10 -0
- package/src/lib/mcpServerLogRepository.ts +48 -0
- package/src/lib/onSignals.ts +39 -0
- package/src/lib/serializeCorsOrigin.ts +14 -0
- package/src/lib/sessionAccessCounter.ts +118 -0
- package/src/server/websocket.ts +121 -0
- package/src/services/encryptionService.ts +309 -0
- package/src/types.ts +4 -0
- package/supergateway.png +0 -0
- package/tests/baseUrl.test.ts +62 -0
- package/tests/concurrency.test.ts +137 -0
- package/tests/helpers/mock-mcp-server.js +94 -0
- package/tests/protocolVersion.test.ts +60 -0
- package/tests/stdioToStatefulStreamableHttp.test.ts +70 -0
- package/tests/stdioToStatelessStreamableHttp.test.ts +71 -0
- package/tests/streamableHttpCli.test.ts +24 -0
- package/tests/streamableHttpToStdio.test.ts +64 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +12 -0
- package/tsconfig.test.json +10 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import cors, { type CorsOptions } from 'cors'
|
|
3
|
+
import { createServer } from 'http'
|
|
4
|
+
import { spawn, ChildProcessWithoutNullStreams } from 'child_process'
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
6
|
+
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
|
|
7
|
+
import { Logger } from '../types.js'
|
|
8
|
+
import { getVersion } from '../lib/getVersion.js'
|
|
9
|
+
import { WebSocketServerTransport } from '../server/websocket.js'
|
|
10
|
+
import { onSignals } from '../lib/onSignals.js'
|
|
11
|
+
import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'
|
|
12
|
+
|
|
13
|
+
export interface StdioToWsArgs {
|
|
14
|
+
stdioCmd: string
|
|
15
|
+
port: number
|
|
16
|
+
messagePath: string
|
|
17
|
+
logger: Logger
|
|
18
|
+
corsOrigin: CorsOptions['origin']
|
|
19
|
+
healthEndpoints: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function stdioToWs(args: StdioToWsArgs) {
|
|
23
|
+
const { stdioCmd, port, messagePath, logger, healthEndpoints, corsOrigin } =
|
|
24
|
+
args
|
|
25
|
+
logger.info(` - port: ${port}`)
|
|
26
|
+
logger.info(` - stdio: ${stdioCmd}`)
|
|
27
|
+
logger.info(` - messagePath: ${messagePath}`)
|
|
28
|
+
logger.info(
|
|
29
|
+
` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`,
|
|
30
|
+
)
|
|
31
|
+
logger.info(
|
|
32
|
+
` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
let wsTransport: WebSocketServerTransport | null = null
|
|
36
|
+
let child: ChildProcessWithoutNullStreams | null = null
|
|
37
|
+
let isReady = false
|
|
38
|
+
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
if (wsTransport) {
|
|
41
|
+
wsTransport.close().catch((err) => {
|
|
42
|
+
logger.error(`Error stopping WebSocket server: ${err.message}`)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
if (child) {
|
|
46
|
+
child.kill()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onSignals({
|
|
51
|
+
logger,
|
|
52
|
+
cleanup,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
child = spawn(stdioCmd, { shell: true })
|
|
57
|
+
child.on('exit', (code, signal) => {
|
|
58
|
+
logger.error(`Child exited: code=${code}, signal=${signal}`)
|
|
59
|
+
cleanup()
|
|
60
|
+
process.exit(code ?? 1)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const server = new Server(
|
|
64
|
+
{ name: 'supergateway', version: getVersion() },
|
|
65
|
+
{ capabilities: {} },
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Handle child process output
|
|
69
|
+
let buffer = ''
|
|
70
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
71
|
+
buffer += chunk.toString('utf8')
|
|
72
|
+
const lines = buffer.split(/\r?\n/)
|
|
73
|
+
buffer = lines.pop() ?? ''
|
|
74
|
+
lines.forEach((line) => {
|
|
75
|
+
if (!line.trim()) return
|
|
76
|
+
try {
|
|
77
|
+
const jsonMsg = JSON.parse(line)
|
|
78
|
+
logger.info(`Child → WebSocket: ${JSON.stringify(jsonMsg)}`)
|
|
79
|
+
// Broadcast to all connected clients
|
|
80
|
+
wsTransport?.send(jsonMsg, jsonMsg.id).catch((err) => {
|
|
81
|
+
logger.error('Failed to broadcast message:', err)
|
|
82
|
+
})
|
|
83
|
+
} catch {
|
|
84
|
+
logger.error(`Child non-JSON: ${line}`)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
90
|
+
logger.info(`Child stderr: ${chunk.toString('utf8')}`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const app = express()
|
|
94
|
+
|
|
95
|
+
if (corsOrigin) {
|
|
96
|
+
app.use(cors({ origin: corsOrigin }))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const ep of healthEndpoints) {
|
|
100
|
+
app.get(ep, (_req, res) => {
|
|
101
|
+
if (child?.killed) {
|
|
102
|
+
res.status(500).send('Child process has been killed')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!isReady) {
|
|
106
|
+
res.status(500).send('Server is not ready')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.send('ok')
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const httpServer = createServer(app)
|
|
114
|
+
|
|
115
|
+
wsTransport = new WebSocketServerTransport({
|
|
116
|
+
path: messagePath,
|
|
117
|
+
server: httpServer,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await server.connect(wsTransport)
|
|
121
|
+
|
|
122
|
+
wsTransport.onmessage = (msg: JSONRPCMessage) => {
|
|
123
|
+
const line = JSON.stringify(msg)
|
|
124
|
+
logger.info(`WebSocket → Child: ${line}`)
|
|
125
|
+
child!.stdin.write(line + '\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
wsTransport.onconnection = (clientId: string) => {
|
|
129
|
+
logger.info(`New WebSocket connection: ${clientId}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
wsTransport.ondisconnection = (clientId: string) => {
|
|
133
|
+
logger.info(`WebSocket connection closed: ${clientId}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
wsTransport.onerror = (err: Error) => {
|
|
137
|
+
logger.error(`WebSocket error: ${err.message}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isReady = true
|
|
141
|
+
|
|
142
|
+
httpServer.listen(port, () => {
|
|
143
|
+
logger.info(`Listening on port ${port}`)
|
|
144
|
+
logger.info(`WebSocket endpoint: ws://localhost:${port}${messagePath}`)
|
|
145
|
+
})
|
|
146
|
+
} catch (err: any) {
|
|
147
|
+
logger.error(`Failed to start: ${err.message}`)
|
|
148
|
+
cleanup()
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
2
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
5
|
+
import type {
|
|
6
|
+
JSONRPCMessage,
|
|
7
|
+
JSONRPCRequest,
|
|
8
|
+
ClientCapabilities,
|
|
9
|
+
Implementation,
|
|
10
|
+
} from '@modelcontextprotocol/sdk/types.js'
|
|
11
|
+
import { InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
12
|
+
import { z } from 'zod'
|
|
13
|
+
import { getVersion } from '../lib/getVersion.js'
|
|
14
|
+
import { Logger } from '../types.js'
|
|
15
|
+
import { onSignals } from '../lib/onSignals.js'
|
|
16
|
+
|
|
17
|
+
export interface StreamableHttpToStdioArgs {
|
|
18
|
+
streamableHttpUrl: string
|
|
19
|
+
logger: Logger
|
|
20
|
+
headers: Record<string, string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let mcpClient: Client | undefined
|
|
24
|
+
|
|
25
|
+
const newInitializeMcpClient = ({ message }: { message: JSONRPCRequest }) => {
|
|
26
|
+
const clientInfo = message.params?.clientInfo as Implementation | undefined
|
|
27
|
+
const clientCapabilities = message.params?.capabilities as
|
|
28
|
+
| ClientCapabilities
|
|
29
|
+
| undefined
|
|
30
|
+
|
|
31
|
+
return new Client(
|
|
32
|
+
{
|
|
33
|
+
name: clientInfo?.name ?? 'supergateway',
|
|
34
|
+
version: clientInfo?.version ?? getVersion(),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
capabilities: clientCapabilities ?? {},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const newFallbackMcpClient = async ({
|
|
43
|
+
mcpTransport,
|
|
44
|
+
}: {
|
|
45
|
+
mcpTransport: StreamableHTTPClientTransport
|
|
46
|
+
}) => {
|
|
47
|
+
const fallbackMcpClient = new Client(
|
|
48
|
+
{
|
|
49
|
+
name: 'supergateway',
|
|
50
|
+
version: getVersion(),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
capabilities: {},
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
await fallbackMcpClient.connect(mcpTransport)
|
|
58
|
+
return fallbackMcpClient
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function streamableHttpToStdio(args: StreamableHttpToStdioArgs) {
|
|
62
|
+
const { streamableHttpUrl, logger, headers } = args
|
|
63
|
+
|
|
64
|
+
logger.info(` - streamableHttp: ${streamableHttpUrl}`)
|
|
65
|
+
logger.info(
|
|
66
|
+
` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`,
|
|
67
|
+
)
|
|
68
|
+
logger.info('Connecting to Streamable HTTP...')
|
|
69
|
+
|
|
70
|
+
onSignals({ logger })
|
|
71
|
+
|
|
72
|
+
const mcpTransport = new StreamableHTTPClientTransport(
|
|
73
|
+
new URL(streamableHttpUrl),
|
|
74
|
+
{
|
|
75
|
+
requestInit: {
|
|
76
|
+
headers,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
mcpTransport.onerror = (err) => {
|
|
82
|
+
logger.error('Streamable HTTP error:', err)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mcpTransport.onclose = () => {
|
|
86
|
+
logger.error('Streamable HTTP connection closed')
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const stdioServer = new Server(
|
|
91
|
+
{
|
|
92
|
+
name: 'supergateway',
|
|
93
|
+
version: getVersion(),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
capabilities: {},
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const stdioTransport = new StdioServerTransport()
|
|
101
|
+
await stdioServer.connect(stdioTransport)
|
|
102
|
+
|
|
103
|
+
const wrapResponse = (req: JSONRPCRequest, payload: object) => ({
|
|
104
|
+
jsonrpc: req.jsonrpc || '2.0',
|
|
105
|
+
id: req.id,
|
|
106
|
+
...payload,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
stdioServer.transport!.onmessage = async (message: JSONRPCMessage) => {
|
|
110
|
+
const isRequest = 'method' in message && 'id' in message
|
|
111
|
+
if (isRequest) {
|
|
112
|
+
logger.info('Stdio → Streamable HTTP:', message)
|
|
113
|
+
const req = message as JSONRPCRequest
|
|
114
|
+
let result
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (!mcpClient) {
|
|
118
|
+
if (message.method === 'initialize') {
|
|
119
|
+
mcpClient = newInitializeMcpClient({
|
|
120
|
+
message,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const originalRequest = mcpClient.request
|
|
124
|
+
|
|
125
|
+
mcpClient.request = async function (
|
|
126
|
+
possibleInitRequestMessage,
|
|
127
|
+
...restArgs
|
|
128
|
+
) {
|
|
129
|
+
if (
|
|
130
|
+
InitializeRequestSchema.safeParse(possibleInitRequestMessage)
|
|
131
|
+
.success &&
|
|
132
|
+
message.params?.protocolVersion
|
|
133
|
+
) {
|
|
134
|
+
// respect the protocol version from the stdio client's init request
|
|
135
|
+
possibleInitRequestMessage.params!.protocolVersion =
|
|
136
|
+
message.params.protocolVersion
|
|
137
|
+
}
|
|
138
|
+
result = await originalRequest.apply(this, [
|
|
139
|
+
possibleInitRequestMessage,
|
|
140
|
+
...restArgs,
|
|
141
|
+
])
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await mcpClient.connect(mcpTransport)
|
|
146
|
+
mcpClient.request = originalRequest
|
|
147
|
+
} else {
|
|
148
|
+
logger.info(
|
|
149
|
+
'Streamable HTTP client not initialized, creating fallback client',
|
|
150
|
+
)
|
|
151
|
+
mcpClient = await newFallbackMcpClient({ mcpTransport })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logger.info('Streamable HTTP connected')
|
|
155
|
+
} else {
|
|
156
|
+
result = await mcpClient.request(req, z.any())
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.error('Request error:', err)
|
|
160
|
+
const errorCode =
|
|
161
|
+
err && typeof err === 'object' && 'code' in err
|
|
162
|
+
? (err as any).code
|
|
163
|
+
: -32000
|
|
164
|
+
let errorMsg =
|
|
165
|
+
err && typeof err === 'object' && 'message' in err
|
|
166
|
+
? (err as any).message
|
|
167
|
+
: 'Internal error'
|
|
168
|
+
const prefix = `MCP error ${errorCode}:`
|
|
169
|
+
if (errorMsg.startsWith(prefix)) {
|
|
170
|
+
errorMsg = errorMsg.slice(prefix.length).trim()
|
|
171
|
+
}
|
|
172
|
+
const errorResp = wrapResponse(req, {
|
|
173
|
+
error: {
|
|
174
|
+
code: errorCode,
|
|
175
|
+
message: errorMsg,
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
process.stdout.write(JSON.stringify(errorResp) + '\n')
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
const response = wrapResponse(
|
|
182
|
+
req,
|
|
183
|
+
result.hasOwnProperty('error')
|
|
184
|
+
? { error: { ...result.error } }
|
|
185
|
+
: { result: { ...result } },
|
|
186
|
+
)
|
|
187
|
+
logger.info('Response:', response)
|
|
188
|
+
process.stdout.write(JSON.stringify(response) + '\n')
|
|
189
|
+
} else {
|
|
190
|
+
logger.info('Streamable HTTP → Stdio:', message)
|
|
191
|
+
process.stdout.write(JSON.stringify(message) + '\n')
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
logger.info('Stdio server listening')
|
|
196
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* index.ts
|
|
4
|
+
*
|
|
5
|
+
* Run MCP stdio servers over SSE, convert between stdio, SSE, WS.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* # stdio→SSE
|
|
9
|
+
* npx -y supergateway --stdio "npx -y @modelcontextprotocol/server-filesystem /" \
|
|
10
|
+
* --port 8000 --baseUrl http://localhost:8000 --ssePath /sse --messagePath /message
|
|
11
|
+
*
|
|
12
|
+
* # SSE→stdio
|
|
13
|
+
* npx -y supergateway --sse "https://mcp-server-ab71a6b2-cd55-49d0-adba-562bc85956e3.supermachine.app"
|
|
14
|
+
*
|
|
15
|
+
* # stdio→WS
|
|
16
|
+
* npx -y supergateway --stdio "npx -y @modelcontextprotocol/server-filesystem /" --outputTransport ws
|
|
17
|
+
*
|
|
18
|
+
* # Streamable HTTP→stdio
|
|
19
|
+
* npx -y supergateway --streamableHttp "https://mcp-server.example.com/mcp"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import yargs from 'yargs'
|
|
23
|
+
import { hideBin } from 'yargs/helpers'
|
|
24
|
+
import { stdioToSse } from './gateways/stdioToSse.js'
|
|
25
|
+
import { sseToStdio } from './gateways/sseToStdio.js'
|
|
26
|
+
import { stdioToWs } from './gateways/stdioToWs.js'
|
|
27
|
+
import { streamableHttpToStdio } from './gateways/streamableHttpToStdio.js'
|
|
28
|
+
import { headers } from './lib/headers.js'
|
|
29
|
+
import { corsOrigin } from './lib/corsOrigin.js'
|
|
30
|
+
import { getLogger } from './lib/getLogger.js'
|
|
31
|
+
import { stdioToStatelessStreamableHttp } from './gateways/stdioToStatelessStreamableHttp.js'
|
|
32
|
+
import { stdioToStatefulStreamableHttp } from './gateways/stdioToStatefulStreamableHttp.js'
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
const argv = yargs(hideBin(process.argv))
|
|
36
|
+
.option('stdio', {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Command to run an MCP server over Stdio',
|
|
39
|
+
})
|
|
40
|
+
.option('sse', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'SSE URL to connect to',
|
|
43
|
+
})
|
|
44
|
+
.option('streamableHttp', {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Streamable HTTP URL to connect to',
|
|
47
|
+
})
|
|
48
|
+
.option('outputTransport', {
|
|
49
|
+
type: 'string',
|
|
50
|
+
choices: ['stdio', 'sse', 'ws', 'streamableHttp'],
|
|
51
|
+
default: () => {
|
|
52
|
+
const args = hideBin(process.argv)
|
|
53
|
+
|
|
54
|
+
if (args.includes('--stdio')) return 'sse'
|
|
55
|
+
if (args.includes('--sse')) return 'stdio'
|
|
56
|
+
if (args.includes('--streamableHttp')) return 'stdio'
|
|
57
|
+
|
|
58
|
+
return undefined
|
|
59
|
+
},
|
|
60
|
+
description:
|
|
61
|
+
'Transport for output. Default is "sse" when using --stdio and "stdio" when using --sse or --streamableHttp.',
|
|
62
|
+
})
|
|
63
|
+
.option('port', {
|
|
64
|
+
type: 'number',
|
|
65
|
+
default: 8000,
|
|
66
|
+
description: '(stdio→SSE, stdio→WS) Port for output MCP server',
|
|
67
|
+
})
|
|
68
|
+
.option('baseUrl', {
|
|
69
|
+
type: 'string',
|
|
70
|
+
default: '',
|
|
71
|
+
description: '(stdio→SSE) Base URL for output MCP server',
|
|
72
|
+
})
|
|
73
|
+
.option('ssePath', {
|
|
74
|
+
type: 'string',
|
|
75
|
+
default: '/sse',
|
|
76
|
+
description: '(stdio→SSE) Path for SSE subscriptions',
|
|
77
|
+
})
|
|
78
|
+
.option('messagePath', {
|
|
79
|
+
type: 'string',
|
|
80
|
+
default: '/message',
|
|
81
|
+
description: '(stdio→SSE, stdio→WS) Path for messages',
|
|
82
|
+
})
|
|
83
|
+
.option('streamableHttpPath', {
|
|
84
|
+
type: 'string',
|
|
85
|
+
default: '/mcp',
|
|
86
|
+
description: '(stdio→StreamableHttp) Path for StreamableHttp',
|
|
87
|
+
})
|
|
88
|
+
.option('logLevel', {
|
|
89
|
+
choices: ['debug', 'info', 'none'] as const,
|
|
90
|
+
default: 'info',
|
|
91
|
+
description: 'Logging level',
|
|
92
|
+
})
|
|
93
|
+
.option('cors', {
|
|
94
|
+
type: 'array',
|
|
95
|
+
description:
|
|
96
|
+
'Enable CORS. Use --cors with no values to allow all origins, or supply one or more allowed origins (e.g. --cors "http://example.com" or --cors "/example\\.com$/" for regex matching).',
|
|
97
|
+
})
|
|
98
|
+
.option('healthEndpoint', {
|
|
99
|
+
type: 'array',
|
|
100
|
+
default: [],
|
|
101
|
+
description:
|
|
102
|
+
'One or more endpoints returning "ok", e.g. --healthEndpoint /healthz --healthEndpoint /readyz',
|
|
103
|
+
})
|
|
104
|
+
.option('header', {
|
|
105
|
+
type: 'array',
|
|
106
|
+
default: [],
|
|
107
|
+
description:
|
|
108
|
+
'Headers to be added to the request headers, e.g. --header "x-user-id: 123"',
|
|
109
|
+
})
|
|
110
|
+
.option('oauth2Bearer', {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description:
|
|
113
|
+
'Authorization header to be added, e.g. --oauth2Bearer "some-access-token" adds "Authorization: Bearer some-access-token"',
|
|
114
|
+
})
|
|
115
|
+
.option('stateful', {
|
|
116
|
+
type: 'boolean',
|
|
117
|
+
default: false,
|
|
118
|
+
description:
|
|
119
|
+
'Whether the server is stateful. Only supported for stdio→StreamableHttp.',
|
|
120
|
+
})
|
|
121
|
+
.option('sessionTimeout', {
|
|
122
|
+
type: 'number',
|
|
123
|
+
description:
|
|
124
|
+
'Session timeout in milliseconds. Only supported for stateful stdio→StreamableHttp. If not set, the session will only be deleted when client transport explicitly terminates the session.',
|
|
125
|
+
})
|
|
126
|
+
.option('protocolVersion', {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description:
|
|
129
|
+
'MCP protocol version to use for auto-initialization. Defaults to "2024-11-05" if not specified.',
|
|
130
|
+
default: '2024-11-05',
|
|
131
|
+
})
|
|
132
|
+
.help()
|
|
133
|
+
.parseSync()
|
|
134
|
+
|
|
135
|
+
const hasStdio = Boolean(argv.stdio)
|
|
136
|
+
const hasSse = Boolean(argv.sse)
|
|
137
|
+
const hasStreamableHttp = Boolean(argv.streamableHttp)
|
|
138
|
+
|
|
139
|
+
const activeCount = [hasStdio, hasSse, hasStreamableHttp].filter(
|
|
140
|
+
Boolean,
|
|
141
|
+
).length
|
|
142
|
+
|
|
143
|
+
const logger = getLogger({
|
|
144
|
+
logLevel: argv.logLevel,
|
|
145
|
+
outputTransport: argv.outputTransport as string,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (activeCount === 0) {
|
|
149
|
+
logger.error(
|
|
150
|
+
'Error: You must specify one of --stdio, --sse, or --streamableHttp',
|
|
151
|
+
)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
} else if (activeCount > 1) {
|
|
154
|
+
logger.error(
|
|
155
|
+
'Error: Specify only one of --stdio, --sse, or --streamableHttp, not multiple',
|
|
156
|
+
)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logger.info('Starting...')
|
|
161
|
+
logger.info(
|
|
162
|
+
'Supergateway is supported by Supermachine (hosted MCPs) - https://supermachine.ai',
|
|
163
|
+
)
|
|
164
|
+
logger.info(` - outputTransport: ${argv.outputTransport}`)
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
if (hasStdio) {
|
|
168
|
+
if (argv.outputTransport === 'sse') {
|
|
169
|
+
await stdioToSse({
|
|
170
|
+
stdioCmd: argv.stdio!,
|
|
171
|
+
port: argv.port,
|
|
172
|
+
baseUrl: argv.baseUrl,
|
|
173
|
+
ssePath: argv.ssePath,
|
|
174
|
+
messagePath: argv.messagePath,
|
|
175
|
+
logger,
|
|
176
|
+
corsOrigin: corsOrigin({ argv }),
|
|
177
|
+
healthEndpoints: argv.healthEndpoint as string[],
|
|
178
|
+
headers: headers({
|
|
179
|
+
argv,
|
|
180
|
+
logger,
|
|
181
|
+
}),
|
|
182
|
+
})
|
|
183
|
+
} else if (argv.outputTransport === 'ws') {
|
|
184
|
+
await stdioToWs({
|
|
185
|
+
stdioCmd: argv.stdio!,
|
|
186
|
+
port: argv.port,
|
|
187
|
+
messagePath: argv.messagePath,
|
|
188
|
+
logger,
|
|
189
|
+
corsOrigin: corsOrigin({ argv }),
|
|
190
|
+
healthEndpoints: argv.healthEndpoint as string[],
|
|
191
|
+
})
|
|
192
|
+
} else if (argv.outputTransport === 'streamableHttp') {
|
|
193
|
+
const stateful = argv.stateful
|
|
194
|
+
if (stateful) {
|
|
195
|
+
logger.info('Running stateful server')
|
|
196
|
+
|
|
197
|
+
let sessionTimeout: null | number
|
|
198
|
+
if (typeof argv.sessionTimeout === 'number') {
|
|
199
|
+
if (argv.sessionTimeout <= 0) {
|
|
200
|
+
logger.error(
|
|
201
|
+
`Error: \`sessionTimeout\` must be a positive number, received: ${argv.sessionTimeout}`,
|
|
202
|
+
)
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
sessionTimeout = argv.sessionTimeout
|
|
207
|
+
} else {
|
|
208
|
+
sessionTimeout = null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await stdioToStatefulStreamableHttp({
|
|
212
|
+
stdioCmd: argv.stdio!,
|
|
213
|
+
port: argv.port,
|
|
214
|
+
streamableHttpPath: argv.streamableHttpPath,
|
|
215
|
+
logger,
|
|
216
|
+
corsOrigin: corsOrigin({ argv }),
|
|
217
|
+
healthEndpoints: argv.healthEndpoint as string[],
|
|
218
|
+
headers: headers({
|
|
219
|
+
argv,
|
|
220
|
+
logger,
|
|
221
|
+
}),
|
|
222
|
+
sessionTimeout,
|
|
223
|
+
})
|
|
224
|
+
} else {
|
|
225
|
+
logger.info('Running stateless server')
|
|
226
|
+
|
|
227
|
+
await stdioToStatelessStreamableHttp({
|
|
228
|
+
stdioCmd: argv.stdio!,
|
|
229
|
+
port: argv.port,
|
|
230
|
+
streamableHttpPath: argv.streamableHttpPath,
|
|
231
|
+
logger,
|
|
232
|
+
corsOrigin: corsOrigin({ argv }),
|
|
233
|
+
healthEndpoints: argv.healthEndpoint as string[],
|
|
234
|
+
headers: headers({
|
|
235
|
+
argv,
|
|
236
|
+
logger,
|
|
237
|
+
}),
|
|
238
|
+
protocolVersion: argv.protocolVersion,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
logger.error(`Error: stdio→${argv.outputTransport} not supported`)
|
|
243
|
+
process.exit(1)
|
|
244
|
+
}
|
|
245
|
+
} else if (hasSse) {
|
|
246
|
+
if (argv.outputTransport === 'stdio') {
|
|
247
|
+
await sseToStdio({
|
|
248
|
+
sseUrl: argv.sse!,
|
|
249
|
+
logger,
|
|
250
|
+
headers: headers({
|
|
251
|
+
argv,
|
|
252
|
+
logger,
|
|
253
|
+
}),
|
|
254
|
+
})
|
|
255
|
+
} else {
|
|
256
|
+
logger.error(`Error: sse→${argv.outputTransport} not supported`)
|
|
257
|
+
process.exit(1)
|
|
258
|
+
}
|
|
259
|
+
} else if (hasStreamableHttp) {
|
|
260
|
+
if (argv.outputTransport === 'stdio') {
|
|
261
|
+
await streamableHttpToStdio({
|
|
262
|
+
streamableHttpUrl: argv.streamableHttp!,
|
|
263
|
+
logger,
|
|
264
|
+
headers: headers({
|
|
265
|
+
argv,
|
|
266
|
+
logger,
|
|
267
|
+
}),
|
|
268
|
+
})
|
|
269
|
+
} else {
|
|
270
|
+
logger.error(
|
|
271
|
+
`Error: streamableHttp→${argv.outputTransport} not supported`,
|
|
272
|
+
)
|
|
273
|
+
process.exit(1)
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
logger.error('Error: Invalid input transport')
|
|
277
|
+
process.exit(1)
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
logger.error('Fatal error:', err)
|
|
281
|
+
process.exit(1)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
main()
|
|
286
|
+
// test commit
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const corsOrigin = ({
|
|
2
|
+
argv,
|
|
3
|
+
}: {
|
|
4
|
+
argv: {
|
|
5
|
+
cors: (string | number)[] | undefined
|
|
6
|
+
}
|
|
7
|
+
}) => {
|
|
8
|
+
if (!argv.cors) {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (argv.cors.length === 0) {
|
|
13
|
+
return '*'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const origins = argv.cors.map((item) => `${item}`)
|
|
17
|
+
|
|
18
|
+
if (origins.includes('*')) return '*'
|
|
19
|
+
|
|
20
|
+
return origins.map((origin) => {
|
|
21
|
+
if (/^\/.*\/$/.test(origin)) {
|
|
22
|
+
const pattern = origin.slice(1, -1)
|
|
23
|
+
try {
|
|
24
|
+
return new RegExp(pattern)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return origin
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return origin
|
|
30
|
+
})
|
|
31
|
+
}
|