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.
Files changed (62) hide show
  1. package/.github/workflows/docker-publish.yaml +79 -0
  2. package/.husky/pre-commit +17 -0
  3. package/.prettierignore +8 -0
  4. package/.prettierrc +5 -0
  5. package/AGENTS.md +29 -0
  6. package/LICENSE +21 -0
  7. package/README.md +348 -0
  8. package/dist/gateways/sseToStdio.js +139 -0
  9. package/dist/gateways/stdioToSse.js +147 -0
  10. package/dist/gateways/stdioToStatefulStreamableHttp.js +188 -0
  11. package/dist/gateways/stdioToStatelessStreamableHttp.js +208 -0
  12. package/dist/gateways/stdioToWs.js +113 -0
  13. package/dist/gateways/streamableHttpToStdio.js +134 -0
  14. package/dist/index.js +266 -0
  15. package/dist/lib/corsOrigin.js +23 -0
  16. package/dist/lib/getLogger.js +44 -0
  17. package/dist/lib/getVersion.js +16 -0
  18. package/dist/lib/headers.js +31 -0
  19. package/dist/lib/onSignals.js +27 -0
  20. package/dist/lib/serializeCorsOrigin.js +6 -0
  21. package/dist/lib/sessionAccessCounter.js +77 -0
  22. package/dist/server/websocket.js +102 -0
  23. package/dist/services/EncryptionService.js +236 -0
  24. package/dist/types.js +1 -0
  25. package/docker/base.Dockerfile +9 -0
  26. package/docker/deno.Dockerfile +2 -0
  27. package/docker/uvx.Dockerfile +3 -0
  28. package/docker-bake.hcl +51 -0
  29. package/package.json +61 -0
  30. package/scripts/decrypt-sample.ts +34 -0
  31. package/scripts/encryption-play.ts +145 -0
  32. package/src/gateways/sseToStdio.ts +195 -0
  33. package/src/gateways/stdioToSse.ts +260 -0
  34. package/src/gateways/stdioToStatefulStreamableHttp.ts +274 -0
  35. package/src/gateways/stdioToStatelessStreamableHttp.ts +303 -0
  36. package/src/gateways/stdioToWs.ts +151 -0
  37. package/src/gateways/streamableHttpToStdio.ts +196 -0
  38. package/src/index.ts +286 -0
  39. package/src/lib/corsOrigin.ts +31 -0
  40. package/src/lib/getLogger.ts +83 -0
  41. package/src/lib/getVersion.ts +17 -0
  42. package/src/lib/headers.ts +55 -0
  43. package/src/lib/initMongoClient.ts +10 -0
  44. package/src/lib/mcpServerLogRepository.ts +48 -0
  45. package/src/lib/onSignals.ts +39 -0
  46. package/src/lib/serializeCorsOrigin.ts +14 -0
  47. package/src/lib/sessionAccessCounter.ts +118 -0
  48. package/src/server/websocket.ts +121 -0
  49. package/src/services/encryptionService.ts +309 -0
  50. package/src/types.ts +4 -0
  51. package/supergateway.png +0 -0
  52. package/tests/baseUrl.test.ts +62 -0
  53. package/tests/concurrency.test.ts +137 -0
  54. package/tests/helpers/mock-mcp-server.js +94 -0
  55. package/tests/protocolVersion.test.ts +60 -0
  56. package/tests/stdioToStatefulStreamableHttp.test.ts +70 -0
  57. package/tests/stdioToStatelessStreamableHttp.test.ts +71 -0
  58. package/tests/streamableHttpCli.test.ts +24 -0
  59. package/tests/streamableHttpToStdio.test.ts +64 -0
  60. package/tsconfig.build.json +8 -0
  61. package/tsconfig.json +12 -0
  62. package/tsconfig.test.json +10 -0
@@ -0,0 +1,83 @@
1
+ import util from 'node:util'
2
+ import { Logger } from '../types.js'
3
+
4
+ const defaultFormatArgs = (args: any[]) => args
5
+
6
+ const log =
7
+ (
8
+ {
9
+ formatArgs = defaultFormatArgs,
10
+ }: {
11
+ formatArgs?: typeof defaultFormatArgs
12
+ } = { formatArgs: defaultFormatArgs },
13
+ ) =>
14
+ (...args: any[]) =>
15
+ console.log('[supergateway]', ...formatArgs(args))
16
+
17
+ const logStderr =
18
+ (
19
+ {
20
+ formatArgs = defaultFormatArgs,
21
+ }: {
22
+ formatArgs?: typeof defaultFormatArgs
23
+ } = { formatArgs: defaultFormatArgs },
24
+ ) =>
25
+ (...args: any[]) =>
26
+ console.error('[supergateway]', ...formatArgs(args))
27
+
28
+ const noneLogger: Logger = {
29
+ info: () => {},
30
+ error: () => {},
31
+ }
32
+
33
+ const infoLogger: Logger = {
34
+ info: log(),
35
+ error: logStderr(),
36
+ }
37
+
38
+ const infoLoggerStdio: Logger = {
39
+ info: logStderr(),
40
+ error: logStderr(),
41
+ }
42
+
43
+ const debugFormatArgs = (args: any[]) =>
44
+ args.map((arg) => {
45
+ if (typeof arg === 'object') {
46
+ return util.inspect(arg, {
47
+ depth: null,
48
+ colors: process.stderr.isTTY,
49
+ compact: false,
50
+ })
51
+ }
52
+
53
+ return arg
54
+ })
55
+
56
+ const debugLogger: Logger = {
57
+ info: log({ formatArgs: debugFormatArgs }),
58
+ error: logStderr({ formatArgs: debugFormatArgs }),
59
+ }
60
+
61
+ const debugLoggerStdio: Logger = {
62
+ info: logStderr({ formatArgs: debugFormatArgs }),
63
+ error: logStderr({ formatArgs: debugFormatArgs }),
64
+ }
65
+
66
+ export const getLogger = ({
67
+ logLevel,
68
+ outputTransport,
69
+ }: {
70
+ logLevel: string
71
+ outputTransport: string
72
+ }): Logger => {
73
+ if (logLevel === 'none') {
74
+ return noneLogger
75
+ }
76
+
77
+ if (logLevel === 'debug') {
78
+ return outputTransport === 'stdio' ? debugLoggerStdio : debugLogger
79
+ }
80
+
81
+ // info logLevel
82
+ return outputTransport === 'stdio' ? infoLoggerStdio : infoLogger
83
+ }
@@ -0,0 +1,17 @@
1
+ import { fileURLToPath } from 'url'
2
+ import { join, dirname } from 'path'
3
+ import { readFileSync } from 'fs'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = dirname(__filename)
7
+
8
+ export function getVersion(): string {
9
+ try {
10
+ const packageJsonPath = join(__dirname, '../../package.json')
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
12
+ return packageJson.version || '1.0.0'
13
+ } catch (err) {
14
+ console.error('[supergateway]', 'Unable to retrieve version:', err)
15
+ return 'unknown'
16
+ }
17
+ }
@@ -0,0 +1,55 @@
1
+ import { Logger } from '../types.js'
2
+
3
+ const parseHeaders = ({
4
+ argvHeader,
5
+ logger,
6
+ }: {
7
+ argvHeader: (string | number)[]
8
+ logger: Logger
9
+ }): Record<string, string> => {
10
+ return argvHeader.reduce<Record<string, string>>((acc, rawHeader) => {
11
+ const header = `${rawHeader}`
12
+
13
+ const colonIndex = header.indexOf(':')
14
+ if (colonIndex === -1) {
15
+ logger.error(`Invalid header format: ${header}, ignoring`)
16
+ return acc
17
+ }
18
+
19
+ const key = header.slice(0, colonIndex).trim()
20
+ const value = header.slice(colonIndex + 1).trim()
21
+
22
+ if (!key || !value) {
23
+ logger.error(`Invalid header format: ${header}, ignoring`)
24
+ return acc
25
+ }
26
+
27
+ acc[key] = value
28
+ return acc
29
+ }, {})
30
+ }
31
+
32
+ export const headers = ({
33
+ argv,
34
+ logger,
35
+ }: {
36
+ argv: {
37
+ header: (string | number)[]
38
+ oauth2Bearer: string | undefined
39
+ }
40
+ logger: Logger
41
+ }): Record<string, string> => {
42
+ const headers = parseHeaders({
43
+ argvHeader: argv.header,
44
+ logger,
45
+ })
46
+
47
+ if ('oauth2Bearer' in argv) {
48
+ return {
49
+ ...headers,
50
+ Authorization: `Bearer ${argv.oauth2Bearer}`,
51
+ }
52
+ }
53
+
54
+ return headers
55
+ }
@@ -0,0 +1,10 @@
1
+ import { MongoClient } from 'mongodb'
2
+
3
+ export function initMongoClient(uri = process.env.MONGO_URI): MongoClient {
4
+ if (!uri) {
5
+ throw new Error('MONGO_URI is not set')
6
+ }
7
+
8
+ const client = new MongoClient(uri)
9
+ return client
10
+ }
@@ -0,0 +1,48 @@
1
+ import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
2
+ import { Collection, MongoClient } from 'mongodb'
3
+
4
+ export class McpServerLogRepository {
5
+ private readonly collection: Collection
6
+
7
+ constructor(
8
+ private readonly client: MongoClient,
9
+ dbName = 'local',
10
+ collectionName = 'mcp_server_logs',
11
+ ) {
12
+ this.collection = this.client.db(dbName).collection(collectionName)
13
+ }
14
+
15
+ insert(data: McpServerLogDto) {
16
+ return this.collection.insertOne(data)
17
+ }
18
+
19
+ update(
20
+ sessionId: string,
21
+ update: {
22
+ result?: unknown
23
+ id: number
24
+ error?: { message: string; code: number }
25
+ },
26
+ ) {
27
+ return this.collection.updateOne(
28
+ { sessionId, 'data.id': update.id },
29
+ {
30
+ $set: {
31
+ 'data.result': update.result,
32
+ 'data.error': update.error,
33
+ updatedAt: new Date(),
34
+ },
35
+ },
36
+ )
37
+ }
38
+ }
39
+
40
+ export type McpServerLogDto = {
41
+ ip: string
42
+ type: 'rpc' | 'error' | 'system'
43
+ userId: string
44
+ sessionId: string
45
+ data: JSONRPCMessage | string
46
+ createdAt: Date
47
+ updatedAt: Date
48
+ }
@@ -0,0 +1,39 @@
1
+ import { Logger } from '../types.js'
2
+
3
+ export interface OnSignalsOptions {
4
+ logger: Logger
5
+ cleanup?: () => void
6
+ }
7
+
8
+ /**
9
+ * Sets up signal handlers for graceful shutdown.
10
+ *
11
+ * @param options Configuration options
12
+ * @param options.logger Logger instance
13
+ * @param options.cleanup Optional cleanup function to be called before exit
14
+ */
15
+ export function onSignals(options: OnSignalsOptions): void {
16
+ const { logger, cleanup } = options
17
+
18
+ const handleSignal = (signal: string) => {
19
+ logger.info(`Caught ${signal}. Exiting...`)
20
+ if (cleanup) {
21
+ cleanup()
22
+ }
23
+ process.exit(0)
24
+ }
25
+
26
+ process.on('SIGINT', () => handleSignal('SIGINT'))
27
+
28
+ process.on('SIGTERM', () => handleSignal('SIGTERM'))
29
+
30
+ process.on('SIGHUP', () => handleSignal('SIGHUP'))
31
+
32
+ process.stdin.on('close', () => {
33
+ logger.info('stdin closed. Exiting...')
34
+ if (cleanup) {
35
+ cleanup()
36
+ }
37
+ process.exit(0)
38
+ })
39
+ }
@@ -0,0 +1,14 @@
1
+ import type { CorsOptions } from 'cors'
2
+
3
+ export const serializeCorsOrigin = ({
4
+ corsOrigin,
5
+ }: {
6
+ corsOrigin: CorsOptions['origin']
7
+ }) =>
8
+ JSON.stringify(corsOrigin, (_key, value) => {
9
+ if (value instanceof RegExp) {
10
+ return value.toString()
11
+ }
12
+
13
+ return value
14
+ })
@@ -0,0 +1,118 @@
1
+ import { Logger } from '../types.js'
2
+
3
+ export class SessionAccessCounter {
4
+ private sessions: Map<
5
+ string,
6
+ { accessCount: number } | { timeout: NodeJS.Timeout }
7
+ > = new Map()
8
+
9
+ constructor(
10
+ public timeout: number,
11
+ public cleanup: (sessionId: string) => unknown,
12
+ public logger: Logger,
13
+ ) {}
14
+
15
+ inc(sessionId: string, reason: string) {
16
+ this.logger.info(
17
+ `SessionAccessCounter.inc() ${sessionId}, caused by ${reason}`,
18
+ )
19
+
20
+ const session = this.sessions.get(sessionId)
21
+
22
+ if (!session) {
23
+ // New session
24
+ this.logger.info(
25
+ `Session access count 0 -> 1 for ${sessionId} (new session)`,
26
+ )
27
+ this.sessions.set(sessionId, { accessCount: 1 })
28
+ return
29
+ }
30
+
31
+ if ('timeout' in session) {
32
+ // Clear pending cleanup and reactivate
33
+ this.logger.info(
34
+ `Session access count 0 -> 1, clearing cleanup timeout for ${sessionId}`,
35
+ )
36
+ clearTimeout(session.timeout)
37
+ this.sessions.set(sessionId, { accessCount: 1 })
38
+ } else {
39
+ // Increment active session
40
+ this.logger.info(
41
+ `Session access count ${session.accessCount} -> ${session.accessCount + 1} for ${sessionId}`,
42
+ )
43
+ session.accessCount++
44
+ }
45
+ }
46
+
47
+ dec(sessionId: string, reason: string) {
48
+ this.logger.info(
49
+ `SessionAccessCounter.dec() ${sessionId}, caused by ${reason}`,
50
+ )
51
+
52
+ const session = this.sessions.get(sessionId)
53
+
54
+ if (!session) {
55
+ this.logger.error(
56
+ `Called dec() on non-existent session ${sessionId}, ignoring`,
57
+ )
58
+ return
59
+ }
60
+
61
+ if ('timeout' in session) {
62
+ this.logger.error(
63
+ `Called dec() on session ${sessionId} that is already pending cleanup, ignoring`,
64
+ )
65
+ return
66
+ }
67
+
68
+ if (session.accessCount <= 0) {
69
+ throw new Error(
70
+ `Invalid access count ${session.accessCount} for session ${sessionId}`,
71
+ )
72
+ }
73
+
74
+ session.accessCount--
75
+ this.logger.info(
76
+ `Session access count ${session.accessCount + 1} -> ${session.accessCount} for ${sessionId}`,
77
+ )
78
+
79
+ if (session.accessCount === 0) {
80
+ this.logger.info(
81
+ `Session access count reached 0, setting cleanup timeout for ${sessionId}`,
82
+ )
83
+
84
+ this.sessions.set(sessionId, {
85
+ timeout: setTimeout(() => {
86
+ this.logger.info(`Session ${sessionId} timed out, cleaning up`)
87
+ this.sessions.delete(sessionId)
88
+ this.cleanup(sessionId)
89
+ }, this.timeout),
90
+ })
91
+ }
92
+ }
93
+
94
+ clear(sessionId: string, runCleanup: boolean, reason: string) {
95
+ this.logger.info(
96
+ `SessionAccessCounter.clear() ${sessionId}, caused by ${reason}`,
97
+ )
98
+
99
+ const session = this.sessions.get(sessionId)
100
+ if (!session) {
101
+ this.logger.info(`Attempted to clear non-existent session ${sessionId}`)
102
+ return
103
+ }
104
+
105
+ // Clear any pending timeout
106
+ if ('timeout' in session) {
107
+ clearTimeout(session.timeout)
108
+ }
109
+
110
+ // Remove from tracking
111
+ this.sessions.delete(sessionId)
112
+
113
+ // Run cleanup if requested
114
+ if (runCleanup) {
115
+ this.cleanup(sessionId)
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ Transport,
3
+ TransportSendOptions,
4
+ } from '@modelcontextprotocol/sdk/shared/transport.js'
5
+ import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
6
+ import { v4 as uuidv4 } from 'uuid'
7
+ import { WebSocket, WebSocketServer } from 'ws'
8
+ import { Server } from 'http'
9
+
10
+ export class WebSocketServerTransport implements Transport {
11
+ private wss!: WebSocketServer
12
+ private clients: Map<string, WebSocket> = new Map()
13
+
14
+ onclose?: () => void
15
+ onerror?: (err: Error) => void
16
+ private messageHandler?: (msg: JSONRPCMessage, clientId: string) => void
17
+ onconnection?: (clientId: string) => void
18
+ ondisconnection?: (clientId: string) => void
19
+
20
+ set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined) {
21
+ this.messageHandler = handler
22
+ ? (msg, clientId) => {
23
+ // @ts-ignore
24
+ if (msg.id === undefined) {
25
+ console.log('Broadcast message:', msg)
26
+ return handler(msg)
27
+ }
28
+ // @ts-ignore
29
+ return handler({
30
+ ...msg,
31
+ // @ts-ignore
32
+ id: clientId + ':' + msg.id,
33
+ })
34
+ }
35
+ : undefined
36
+ }
37
+
38
+ constructor({ path, server }: { path: string; server: Server }) {
39
+ this.wss = new WebSocketServer({
40
+ path,
41
+ server,
42
+ })
43
+ }
44
+
45
+ async start(): Promise<void> {
46
+ this.wss.on('connection', (ws: WebSocket) => {
47
+ const clientId = uuidv4()
48
+ this.clients.set(clientId, ws)
49
+ this.onconnection?.(clientId)
50
+
51
+ ws.on('message', (data: Buffer) => {
52
+ try {
53
+ const msg = JSON.parse(data.toString())
54
+ this.messageHandler?.(msg, clientId)
55
+ } catch (err) {
56
+ this.onerror?.(new Error(`Failed to parse message: ${err}`))
57
+ }
58
+ })
59
+
60
+ ws.on('close', () => {
61
+ this.clients.delete(clientId)
62
+ this.ondisconnection?.(clientId)
63
+ })
64
+
65
+ ws.on('error', (err: Error) => {
66
+ this.onerror?.(err)
67
+ })
68
+ })
69
+ }
70
+
71
+ async send(
72
+ msg: JSONRPCMessage,
73
+ options?: TransportSendOptions | string,
74
+ ): Promise<void> {
75
+ // decide if they passed a raw clientId (legacy) or options object
76
+ const clientId = typeof options === 'string' ? options : undefined
77
+
78
+ // if your protocol mangles IDs to include clientId, strip it off
79
+ const [cId, rawId] = clientId?.split(':') ?? []
80
+ if (rawId !== undefined) {
81
+ // @ts-ignore
82
+ msg.id = parseInt(rawId, 10)
83
+ }
84
+
85
+ const payload = JSON.stringify(msg)
86
+
87
+ if (cId) {
88
+ // send only to the one client
89
+ const ws = this.clients.get(cId)
90
+ if (ws?.readyState === WebSocket.OPEN) {
91
+ ws.send(payload)
92
+ } else {
93
+ this.clients.delete(cId)
94
+ this.ondisconnection?.(cId)
95
+ }
96
+ } else {
97
+ // broadcast to everyone
98
+ for (const [id, ws] of this.clients) {
99
+ if (ws.readyState === WebSocket.OPEN) {
100
+ ws.send(payload)
101
+ } else {
102
+ this.clients.delete(id)
103
+ this.ondisconnection?.(id)
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ async broadcast(msg: JSONRPCMessage): Promise<void> {
110
+ return this.send(msg)
111
+ }
112
+
113
+ async close(): Promise<void> {
114
+ return new Promise((resolve) => {
115
+ this.wss.close(() => {
116
+ this.clients.clear()
117
+ resolve()
118
+ })
119
+ })
120
+ }
121
+ }