@platf/bridge 0.0.16 → 0.0.18

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 (40) hide show
  1. package/dist/gateways/statefulBridge.d.ts +10 -7
  2. package/dist/gateways/statefulBridge.js +79 -139
  3. package/dist/gateways/statefulBridge.js.map +1 -1
  4. package/dist/gateways/statelessBridge.d.ts +10 -7
  5. package/dist/gateways/statelessBridge.js +85 -166
  6. package/dist/gateways/statelessBridge.js.map +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/childProcess.d.ts +37 -0
  9. package/dist/lib/childProcess.js +80 -0
  10. package/dist/lib/childProcess.js.map +1 -0
  11. package/dist/lib/config.d.ts +6 -0
  12. package/dist/lib/config.js +7 -0
  13. package/dist/lib/config.js.map +1 -0
  14. package/dist/lib/discoveryRoutes.d.ts +3 -1
  15. package/dist/lib/discoveryRoutes.js +7 -68
  16. package/dist/lib/discoveryRoutes.js.map +1 -1
  17. package/dist/lib/express.d.ts +33 -0
  18. package/dist/lib/express.js +65 -0
  19. package/dist/lib/express.js.map +1 -0
  20. package/dist/lib/getLogger.d.ts +2 -2
  21. package/dist/lib/getLogger.js.map +1 -1
  22. package/dist/lib/mcpMessages.d.ts +10 -0
  23. package/dist/lib/mcpMessages.js +35 -0
  24. package/dist/lib/mcpMessages.js.map +1 -0
  25. package/dist/lib/oauthProxy.d.ts +14 -0
  26. package/dist/lib/oauthProxy.js +80 -0
  27. package/dist/lib/oauthProxy.js.map +1 -0
  28. package/dist/types.d.ts +3 -0
  29. package/package.json +1 -1
  30. package/src/gateways/statefulBridge.ts +87 -167
  31. package/src/gateways/statelessBridge.ts +100 -218
  32. package/src/index.ts +2 -2
  33. package/src/lib/childProcess.ts +120 -0
  34. package/src/lib/config.ts +9 -0
  35. package/src/lib/discoveryRoutes.ts +7 -71
  36. package/src/lib/express.ts +82 -0
  37. package/src/lib/getLogger.ts +4 -3
  38. package/src/lib/mcpMessages.ts +42 -0
  39. package/src/lib/oauthProxy.ts +86 -0
  40. package/src/types.ts +4 -0
@@ -1,19 +1,22 @@
1
- import express from 'express'
2
- import cors, { type CorsOptions } from 'cors'
3
- import { spawn } from 'child_process'
1
+ /**
2
+ * Stateless stdio-to-Streamable HTTP bridge.
3
+ *
4
+ * For every incoming POST request, spawns a fresh child process,
5
+ * auto-initializes it if the request is not an initialize request,
6
+ * and proxies JSON-RPC messages between the HTTP transport and the
7
+ * child's stdin/stdout.
8
+ */
9
+
4
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js'
5
11
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
6
- import {
7
- type JSONRPCMessage,
8
- isInitializeRequest,
9
- } from '@modelcontextprotocol/sdk/types.js'
12
+ import { type JSONRPCMessage, isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
13
+ import type { CorsOptions } from 'cors'
10
14
  import type { AuthConfig, Logger } from '../types.js'
15
+ import { createApp, setResponseHeaders } from '../lib/express.js'
11
16
  import { onSignals } from '../lib/onSignals.js'
12
- import { serializeCorsOrigin } from '../lib/cors.js'
13
- import { createAuthMiddleware } from '../lib/authMiddleware.js'
14
- import { createDiscoveryRouter } from '../lib/discoveryRoutes.js'
15
-
16
- const VERSION = '1.0.0'
17
+ import { spawnManagedChild } from '../lib/childProcess.js'
18
+ import { createInitializeRequest, createInitializedNotification, generateAutoInitId } from '../lib/mcpMessages.js'
19
+ import { VERSION, SERVER_NAME } from '../lib/config.js'
17
20
 
18
21
  export interface StatelessBridgeArgs {
19
22
  stdioCmd: string
@@ -27,225 +30,115 @@ export interface StatelessBridgeArgs {
27
30
  auth: AuthConfig | null
28
31
  }
29
32
 
30
- const setResponseHeaders = (res: express.Response, headers: Record<string, string>) =>
31
- Object.entries(headers).forEach(([key, value]) => res.setHeader(key, value))
32
-
33
- /** Create a synthetic MCP initialize request */
34
- const createInitializeRequest = (
35
- id: string | number,
36
- protocolVersion: string,
37
- ): JSONRPCMessage => ({
38
- jsonrpc: '2.0',
39
- id,
40
- method: 'initialize',
41
- params: {
42
- protocolVersion,
43
- capabilities: {
44
- roots: { listChanged: true },
45
- sampling: {},
46
- },
47
- clientInfo: {
48
- name: 'platf-bridge',
49
- version: VERSION,
50
- },
51
- },
52
- })
53
-
54
- const createInitializedNotification = (): JSONRPCMessage => ({
55
- jsonrpc: '2.0',
56
- method: 'notifications/initialized',
57
- })
58
-
59
33
  /**
60
- * Stateless stdio-to-Streamable HTTP bridge.
61
- *
62
- * For every incoming POST request, spawns a fresh child process,
63
- * auto-initializes it if the request is not an initialize request,
64
- * and proxies JSON-RPC messages between the HTTP transport and the
65
- * child's stdin/stdout.
34
+ * Start the stateless bridge server.
66
35
  */
67
36
  export async function startStatelessBridge(args: StatelessBridgeArgs) {
68
- const {
69
- stdioCmd,
70
- port,
71
- path,
72
- logger,
73
- corsOrigin,
74
- healthEndpoints,
75
- headers,
76
- protocolVersion,
77
- auth,
78
- } = args
37
+ const { stdioCmd, port, path, logger, corsOrigin, healthEndpoints, headers, protocolVersion, auth } = args
79
38
 
80
- logger.info(`[stateless] Starting platf-bridge`)
81
- logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`)
39
+ logger.info(`[stateless] Starting ${SERVER_NAME}`)
82
40
  logger.info(` - port: ${port}`)
83
41
  logger.info(` - stdio: ${stdioCmd}`)
84
42
  logger.info(` - path: ${path}`)
85
43
  logger.info(` - protocolVersion: ${protocolVersion}`)
86
- logger.info(
87
- ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin(corsOrigin)})` : 'disabled'}`,
88
- )
89
- logger.info(
90
- ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`,
91
- )
44
+ logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`)
92
45
 
93
46
  onSignals({ logger })
94
47
 
95
- const app = express()
96
- app.set('trust proxy', true)
97
- app.use(express.json())
98
-
99
- if (corsOrigin) {
100
- app.use(cors({ origin: corsOrigin }))
101
- }
102
-
103
- for (const ep of healthEndpoints) {
104
- app.get(ep, (_req, res) => {
105
- setResponseHeaders(res, headers)
106
- res.send('ok')
107
- })
108
- }
109
-
110
- // --- OAuth discovery & auth middleware (when auth is enabled) ---
111
- if (auth) {
112
- app.use(createDiscoveryRouter(auth, logger))
113
- app.use(path, createAuthMiddleware(auth, logger))
114
- logger.info(` - Auth: enabled (issuer=${auth.issuer})`)
115
- }
48
+ const app = createApp({
49
+ logger,
50
+ corsOrigin,
51
+ healthEndpoints,
52
+ headers,
53
+ auth,
54
+ mcpPath: path,
55
+ })
116
56
 
57
+ // POST handler — spawn child, proxy messages
117
58
  app.post(path, async (req, res) => {
118
59
  try {
119
- const server = new Server(
120
- { name: 'platf-bridge', version: VERSION },
121
- { capabilities: {} },
122
- )
123
- const transport = new StreamableHTTPServerTransport({
124
- sessionIdGenerator: undefined,
125
- })
126
-
60
+ const server = new Server({ name: SERVER_NAME, version: VERSION }, { capabilities: {} })
61
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
127
62
  await server.connect(transport)
128
- const child = spawn(stdioCmd, { shell: true })
129
-
130
- const pendingRequestIds = new Set<string | number>()
131
- let stderrOutput = ''
132
-
133
- child.on('exit', (code, signal) => {
134
- logger.error(`Child exited: code=${code}, signal=${signal}`)
135
-
136
- // Include queued original message's ID if it was never sent to the child
137
- if (pendingOriginalMessage && 'id' in pendingOriginalMessage && pendingOriginalMessage.id !== undefined) {
138
- pendingRequestIds.add(pendingOriginalMessage.id as string | number)
139
- pendingOriginalMessage = null
140
- }
141
-
142
- // Remove auto-init ID — the client doesn't expect a response for it
143
- if (isAutoInitializing && initializeRequestId !== null) {
144
- pendingRequestIds.delete(initializeRequestId)
145
- }
146
-
147
- // Send JSON-RPC error responses for all pending client requests
148
- for (const id of pendingRequestIds) {
149
- const detail = stderrOutput.trim().slice(0, 1000)
150
- const message = detail
151
- ? `Child process exited (code=${code}): ${detail}`
152
- : `Child process exited unexpectedly (code=${code}, signal=${signal})`
153
- try {
154
- transport.send({
155
- jsonrpc: '2.0',
156
- error: { code: -32603, message },
157
- id,
158
- } as JSONRPCMessage)
159
- } catch (e) {
160
- logger.error(`Failed to send error response for request ${id}`, e)
161
- }
162
- }
163
- pendingRequestIds.clear()
164
-
165
- transport.close()
166
- })
167
63
 
168
- // --- Auto-initialization state ---
64
+ // Auto-initialization state
169
65
  let isInitialized = false
170
66
  let initializeRequestId: string | number | null = null
171
67
  let isAutoInitializing = false
172
68
  let pendingOriginalMessage: JSONRPCMessage | null = null
173
69
 
174
- let buffer = ''
175
- child.stdout.on('data', (chunk: Buffer) => {
176
- buffer += chunk.toString('utf8')
177
- const lines = buffer.split(/\r?\n/)
178
- buffer = lines.pop() ?? ''
179
-
180
- for (const line of lines) {
181
- if (!line.trim()) continue
182
- try {
183
- const jsonMsg = JSON.parse(line)
184
- if ('id' in jsonMsg && jsonMsg.id !== undefined) {
185
- pendingRequestIds.delete(jsonMsg.id)
186
- }
187
- logger.info('Child → HTTP:', line)
188
-
189
- // Handle initialize response (auto or client-initiated)
190
- if (initializeRequestId && jsonMsg.id === initializeRequestId) {
191
- logger.info('Initialize response received')
192
- isInitialized = true
193
-
194
- if (isAutoInitializing) {
195
- // Send initialized notification then the queued original message
196
- const notification = createInitializedNotification()
197
- logger.info(`HTTP → Child (initialized): ${JSON.stringify(notification)}`)
198
- child.stdin.write(JSON.stringify(notification) + '\n')
70
+ const managedChild = spawnManagedChild(stdioCmd, logger, {
71
+ onMessage: (msg) => {
72
+ // Handle initialize response (auto or client-initiated)
73
+ if (initializeRequestId && 'id' in msg && msg.id === initializeRequestId) {
74
+ logger.info('[stateless] Initialize response received')
75
+ isInitialized = true
199
76
 
200
- if (pendingOriginalMessage) {
201
- logger.info(`HTTP Child (original): ${JSON.stringify(pendingOriginalMessage)}`)
202
- child.stdin.write(JSON.stringify(pendingOriginalMessage) + '\n')
203
- pendingOriginalMessage = null
204
- }
77
+ if (isAutoInitializing) {
78
+ // Send initialized notification then the queued original message
79
+ managedChild.send(createInitializedNotification())
205
80
 
206
- isAutoInitializing = false
207
- initializeRequestId = null
208
- return // don't forward auto-init response to client
81
+ if (pendingOriginalMessage) {
82
+ managedChild.send(pendingOriginalMessage)
83
+ pendingOriginalMessage = null
209
84
  }
210
85
 
86
+ isAutoInitializing = false
211
87
  initializeRequestId = null
88
+ return // don't forward auto-init response to client
212
89
  }
213
90
 
214
- try {
215
- transport.send(jsonMsg)
216
- } catch (e) {
217
- logger.error('Failed to send to HTTP transport', e)
218
- }
219
- } catch {
220
- logger.error(`Child non-JSON: ${line}`)
91
+ initializeRequestId = null
92
+ }
93
+
94
+ transport.send(msg).catch((e) => {
95
+ logger.error('[stateless] Failed to send to HTTP transport', e)
96
+ })
97
+ },
98
+
99
+ onExit: (code, signal, stderrOutput) => {
100
+ // Include queued original message's ID if never sent
101
+ if (pendingOriginalMessage && 'id' in pendingOriginalMessage && pendingOriginalMessage.id !== undefined) {
102
+ managedChild.trackRequest(pendingOriginalMessage.id as string | number)
103
+ pendingOriginalMessage = null
104
+ }
105
+
106
+ // Remove auto-init ID — client doesn't expect a response for it
107
+ if (isAutoInitializing && initializeRequestId !== null) {
108
+ managedChild.completeRequest(initializeRequestId)
221
109
  }
222
- }
223
- })
224
110
 
225
- child.stderr.on('data', (chunk: Buffer) => {
226
- const text = chunk.toString('utf8')
227
- stderrOutput += text
228
- logger.error(`Child stderr: ${text}`)
111
+ // Send error responses for all pending requests
112
+ for (const id of managedChild.getPendingRequests()) {
113
+ const detail = stderrOutput.trim().slice(0, 1000)
114
+ const message = detail
115
+ ? `Child process exited (code=${code}): ${detail}`
116
+ : `Child process exited unexpectedly (code=${code}, signal=${signal})`
117
+ transport.send({
118
+ jsonrpc: '2.0',
119
+ error: { code: -32603, message },
120
+ id,
121
+ } as JSONRPCMessage).catch(() => {})
122
+ }
123
+
124
+ transport.close()
125
+ },
229
126
  })
230
127
 
231
128
  transport.onmessage = (msg: JSONRPCMessage) => {
232
- logger.info(`HTTP → Child: ${JSON.stringify(msg)}`)
233
-
234
129
  // Track client request IDs for error reporting on child exit
235
130
  if ('id' in msg && msg.id !== undefined) {
236
- pendingRequestIds.add(msg.id as string | number)
131
+ managedChild.trackRequest(msg.id as string | number)
237
132
  }
238
133
 
239
- // Auto-initialize if the first message is not an initialize request
134
+ // Auto-initialize if first message is not an initialize request
240
135
  if (!isInitialized && !isInitializeRequest(msg)) {
241
136
  pendingOriginalMessage = msg
242
- initializeRequestId = `init_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`
137
+ initializeRequestId = generateAutoInitId()
243
138
  isAutoInitializing = true
244
139
 
245
- logger.info('Non-initialize message detected, sending auto-initialize first')
246
- const initReq = createInitializeRequest(initializeRequestId, protocolVersion)
247
- logger.info(`HTTP → Child (auto-init): ${JSON.stringify(initReq)}`)
248
- child.stdin.write(JSON.stringify(initReq) + '\n')
140
+ logger.info('[stateless] Non-initialize message detected, sending auto-initialize first')
141
+ managedChild.send(createInitializeRequest(initializeRequestId, protocolVersion))
249
142
  return
250
143
  }
251
144
 
@@ -253,25 +146,25 @@ export async function startStatelessBridge(args: StatelessBridgeArgs) {
253
146
  if (isInitializeRequest(msg) && 'id' in msg && msg.id !== undefined) {
254
147
  initializeRequestId = msg.id
255
148
  isAutoInitializing = false
256
- logger.info(`Tracking initialize request ID: ${msg.id}`)
149
+ logger.info(`[stateless] Tracking initialize request ID: ${msg.id}`)
257
150
  }
258
151
 
259
- child.stdin.write(JSON.stringify(msg) + '\n')
152
+ managedChild.send(msg)
260
153
  }
261
154
 
262
155
  transport.onclose = () => {
263
- logger.info('HTTP connection closed')
264
- child.kill()
156
+ logger.info('[stateless] HTTP connection closed')
157
+ managedChild.kill()
265
158
  }
266
159
 
267
160
  transport.onerror = (err) => {
268
- logger.error('HTTP transport error:', err)
269
- child.kill()
161
+ logger.error('[stateless] HTTP transport error:', err)
162
+ managedChild.kill()
270
163
  }
271
164
 
272
165
  await transport.handleRequest(req, res, req.body)
273
166
  } catch (error) {
274
- logger.error('Error handling MCP request:', error)
167
+ logger.error('[stateless] Error handling MCP request:', error)
275
168
  if (!res.headersSent) {
276
169
  res.status(500).json({
277
170
  jsonrpc: '2.0',
@@ -282,27 +175,16 @@ export async function startStatelessBridge(args: StatelessBridgeArgs) {
282
175
  }
283
176
  })
284
177
 
285
- app.get(path, (_req, res) => {
286
- logger.info('Received GET method not allowed in stateless mode')
287
- res.writeHead(405).end(
288
- JSON.stringify({
289
- jsonrpc: '2.0',
290
- error: { code: -32000, message: 'Method not allowed.' },
291
- id: null,
292
- }),
293
- )
294
- })
295
-
296
- app.delete(path, (_req, res) => {
297
- logger.info('Received DELETE — method not allowed in stateless mode')
298
- res.writeHead(405).end(
299
- JSON.stringify({
300
- jsonrpc: '2.0',
301
- error: { code: -32000, message: 'Method not allowed.' },
302
- id: null,
303
- }),
304
- )
305
- })
178
+ // GET/DELETE not allowed in stateless mode
179
+ const methodNotAllowed = (_req: any, res: any) => {
180
+ res.writeHead(405).end(JSON.stringify({
181
+ jsonrpc: '2.0',
182
+ error: { code: -32000, message: 'Method not allowed.' },
183
+ id: null,
184
+ }))
185
+ }
186
+ app.get(path, methodNotAllowed)
187
+ app.delete(path, methodNotAllowed)
306
188
 
307
189
  app.listen(port, () => {
308
190
  logger.info(`Listening on port ${port}`)
package/src/index.ts CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  import yargs from 'yargs'
4
4
  import { hideBin } from 'yargs/helpers'
5
- import { getLogger, type LogLevel } from './lib/getLogger.js'
5
+ import { getLogger } from './lib/getLogger.js'
6
6
  import { parseCorsOrigin } from './lib/cors.js'
7
7
  import { parseHeaders } from './lib/headers.js'
8
8
  import { startStatelessBridge } from './gateways/statelessBridge.js'
9
9
  import { startStatefulBridge } from './gateways/statefulBridge.js'
10
- import type { AuthConfig } from './types.js'
10
+ import type { AuthConfig, LogLevel } from './types.js'
11
11
 
12
12
  const argv = await yargs(hideBin(process.argv))
13
13
  .scriptName('platf-bridge')
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Child process management for stdio-based MCP servers.
3
+ *
4
+ * Handles spawning child processes, parsing JSON-RPC messages from stdout,
5
+ * and writing messages to stdin.
6
+ */
7
+
8
+ import { spawn, type ChildProcess } from 'child_process'
9
+ import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
10
+ import type { Logger } from '../types.js'
11
+
12
+ export interface ChildProcessCallbacks {
13
+ /** Called when a JSON-RPC message is received from the child */
14
+ onMessage: (msg: JSONRPCMessage) => void
15
+ /** Called when the child process exits */
16
+ onExit: (code: number | null, signal: string | null, stderrOutput: string) => void
17
+ /** Called when there's an error spawning or communicating with the child */
18
+ onError?: (err: Error) => void
19
+ }
20
+
21
+ export interface ManagedChildProcess {
22
+ /** The underlying child process */
23
+ child: ChildProcess
24
+ /** Send a JSON-RPC message to the child's stdin */
25
+ send: (msg: JSONRPCMessage) => boolean
26
+ /** Kill the child process */
27
+ kill: () => void
28
+ /** Track a request ID as pending (for error reporting on exit) */
29
+ trackRequest: (id: string | number) => void
30
+ /** Mark a request ID as completed */
31
+ completeRequest: (id: string | number) => void
32
+ /** Get all pending request IDs */
33
+ getPendingRequests: () => Set<string | number>
34
+ }
35
+
36
+ /**
37
+ * Spawn a child process and set up JSON-RPC message handling.
38
+ *
39
+ * The child is expected to speak JSON-RPC over stdio (one message per line).
40
+ */
41
+ export function spawnManagedChild(
42
+ command: string,
43
+ logger: Logger,
44
+ callbacks: ChildProcessCallbacks,
45
+ ): ManagedChildProcess {
46
+ const pendingRequestIds = new Set<string | number>()
47
+ let stderrOutput = ''
48
+ let buffer = ''
49
+
50
+ const child = spawn(command, { shell: true, stdio: ['pipe', 'pipe', 'pipe'] })
51
+
52
+ logger.info(`[child] Spawned process, pid=${child.pid}`)
53
+
54
+ child.on('spawn', () => {
55
+ logger.info(`[child] spawn event fired, pid=${child.pid}`)
56
+ })
57
+
58
+ child.on('error', (err) => {
59
+ logger.error(`[child] error event: ${err.message}`)
60
+ callbacks.onError?.(err)
61
+ })
62
+
63
+ child.on('exit', (code, signal) => {
64
+ logger.info(`[child] exited: code=${code}, signal=${signal}`)
65
+ callbacks.onExit(code, signal, stderrOutput)
66
+ })
67
+
68
+ child.stdout.on('data', (chunk: Buffer) => {
69
+ const chunkStr = chunk.toString('utf8')
70
+ logger.info(`[child] stdout received ${chunk.length} bytes`)
71
+ buffer += chunkStr
72
+
73
+ const lines = buffer.split(/\r?\n/)
74
+ buffer = lines.pop() ?? ''
75
+
76
+ for (const line of lines) {
77
+ if (!line.trim()) continue
78
+ try {
79
+ const jsonMsg = JSON.parse(line) as JSONRPCMessage
80
+ // Auto-complete request tracking for responses
81
+ if ('id' in jsonMsg && jsonMsg.id !== undefined) {
82
+ pendingRequestIds.delete(jsonMsg.id)
83
+ }
84
+ logger.info(`[child] → message: ${line.slice(0, 200)}${line.length > 200 ? '...' : ''}`)
85
+ callbacks.onMessage(jsonMsg)
86
+ } catch {
87
+ logger.error(`[child] non-JSON output: ${line}`)
88
+ }
89
+ }
90
+ })
91
+
92
+ child.stderr.on('data', (chunk: Buffer) => {
93
+ const text = chunk.toString('utf8')
94
+ stderrOutput += text
95
+ logger.error(`[child] stderr: ${text}`)
96
+ })
97
+
98
+ child.stdin.on('error', (err) => {
99
+ logger.error(`[child] stdin error: ${err.message}`)
100
+ })
101
+
102
+ return {
103
+ child,
104
+ send: (msg: JSONRPCMessage) => {
105
+ const payload = JSON.stringify(msg) + '\n'
106
+ logger.info(`[child] ← message: ${JSON.stringify(msg).slice(0, 200)}`)
107
+ return child.stdin.write(payload)
108
+ },
109
+ kill: () => {
110
+ child.kill()
111
+ },
112
+ trackRequest: (id: string | number) => {
113
+ pendingRequestIds.add(id)
114
+ },
115
+ completeRequest: (id: string | number) => {
116
+ pendingRequestIds.delete(id)
117
+ },
118
+ getPendingRequests: () => pendingRequestIds,
119
+ }
120
+ }
@@ -0,0 +1,9 @@
1
+ /** Shared configuration constants */
2
+
3
+ export const VERSION = '1.0.0'
4
+
5
+ /** Default MCP protocol version for auto-initialization */
6
+ export const DEFAULT_PROTOCOL_VERSION = '2025-03-26'
7
+
8
+ /** Bridge server name used in MCP handshakes */
9
+ export const SERVER_NAME = 'platf-bridge'
@@ -4,7 +4,9 @@
4
4
  * When auth is enabled these routes expose:
5
5
  * - GET /.well-known/oauth-protected-resource[/*] (RFC 9728)
6
6
  * - GET /.well-known/oauth-authorization-server[/*] (RFC 8414 — proxied from issuer)
7
- * - POST /oauth/register (Pseudo-DCR — RFC 7591)
7
+ * - POST /register (Pseudo-DCR — RFC 7591)
8
+ *
9
+ * OAuth proxy routes (/authorize, /token, /jwks) are in oauthProxy.ts.
8
10
  *
9
11
  * These endpoints are unauthenticated — they must be accessible to
10
12
  * any client performing OAuth discovery before obtaining a token.
@@ -92,9 +94,9 @@ export function createDiscoveryRouter(auth: AuthConfig, logger: Logger): Router
92
94
  const host = req.get('host')
93
95
  const bridgeOrigin = `${scheme}://${host}`
94
96
  metadata.issuer = bridgeOrigin
95
- metadata.authorization_endpoint = `${bridgeOrigin}/oauth/authorize`
96
- metadata.token_endpoint = `${bridgeOrigin}/oauth/token`
97
- metadata.registration_endpoint = `${bridgeOrigin}/oauth/register`
97
+ metadata.authorization_endpoint = `${bridgeOrigin}/authorize`
98
+ metadata.token_endpoint = `${bridgeOrigin}/token`
99
+ metadata.registration_endpoint = `${bridgeOrigin}/register`
98
100
  metadata.jwks_uri = `${bridgeOrigin}/jwks`
99
101
 
100
102
  res.json(metadata)
@@ -112,7 +114,7 @@ export function createDiscoveryRouter(auth: AuthConfig, logger: Logger): Router
112
114
  * (e.g., VS Code Copilot) discover the correct client_id through
113
115
  * the normal DCR flow without requiring out-of-band configuration.
114
116
  */
115
- router.post('/oauth/register', (req: Request, res: Response) => {
117
+ router.post('/register', (req: Request, res: Response) => {
116
118
  const body = req.body ?? {}
117
119
  res.status(201).json({
118
120
  client_id: auth.clientId,
@@ -125,71 +127,5 @@ export function createDiscoveryRouter(auth: AuthConfig, logger: Logger): Router
125
127
  })
126
128
  })
127
129
 
128
- /**
129
- * OAuth Authorization Endpoint — Redirect to upstream
130
- *
131
- * Since the bridge advertises itself as the authorization_server,
132
- * clients will attempt to call /oauth/authorize here. We redirect
133
- * to the upstream auth server, preserving all query parameters.
134
- */
135
- router.get('/oauth/authorize', (req: Request, res: Response) => {
136
- const upstreamUrl = new URL(`${auth.issuer}/oauth/authorize`)
137
- // Copy all query params to upstream
138
- for (const [key, value] of Object.entries(req.query)) {
139
- if (typeof value === 'string') {
140
- upstreamUrl.searchParams.set(key, value)
141
- }
142
- }
143
- logger.info(`[discovery] Redirecting /oauth/authorize to ${upstreamUrl.toString().slice(0, 100)}...`)
144
- res.redirect(upstreamUrl.toString())
145
- })
146
-
147
- /**
148
- * OAuth Token Endpoint — Proxy to upstream
149
- *
150
- * Proxies token exchange requests to the upstream auth server.
151
- */
152
- router.post('/oauth/token', async (req: Request, res: Response) => {
153
- try {
154
- const upstreamUrl = `${auth.issuer}/oauth/token`
155
- logger.info('[discovery] Proxying /oauth/token to upstream')
156
-
157
- const upstreamRes = await fetch(upstreamUrl, {
158
- method: 'POST',
159
- headers: {
160
- 'Content-Type': req.get('Content-Type') || 'application/x-www-form-urlencoded',
161
- },
162
- body: req.get('Content-Type')?.includes('application/json')
163
- ? JSON.stringify(req.body)
164
- : new URLSearchParams(req.body as Record<string, string>).toString(),
165
- })
166
-
167
- const data = await upstreamRes.text()
168
- res.status(upstreamRes.status)
169
- res.set('Content-Type', upstreamRes.headers.get('Content-Type') || 'application/json')
170
- res.send(data)
171
- } catch (err: any) {
172
- logger.error('[discovery] Error proxying /oauth/token:', err.message ?? err)
173
- res.status(502).json({ error: 'upstream_error' })
174
- }
175
- })
176
-
177
- /**
178
- * JWKS Endpoint — Proxy to upstream
179
- *
180
- * Proxies JSON Web Key Set requests for token verification.
181
- */
182
- router.get('/jwks', async (req: Request, res: Response) => {
183
- try {
184
- const upstreamUrl = `${auth.issuer}/jwks`
185
- const upstreamRes = await fetch(upstreamUrl)
186
- const data = await upstreamRes.json()
187
- res.json(data)
188
- } catch (err: any) {
189
- logger.error('[discovery] Error proxying /jwks:', err.message ?? err)
190
- res.status(502).json({ error: 'upstream_error' })
191
- }
192
- })
193
-
194
130
  return router
195
131
  }