@platf/bridge 0.0.16 → 0.0.17
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/dist/gateways/statefulBridge.d.ts +10 -7
- package/dist/gateways/statefulBridge.js +79 -139
- package/dist/gateways/statefulBridge.js.map +1 -1
- package/dist/gateways/statelessBridge.d.ts +10 -7
- package/dist/gateways/statelessBridge.js +85 -166
- package/dist/gateways/statelessBridge.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/childProcess.d.ts +37 -0
- package/dist/lib/childProcess.js +80 -0
- package/dist/lib/childProcess.js.map +1 -0
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.js +7 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/discoveryRoutes.d.ts +2 -0
- package/dist/lib/discoveryRoutes.js +2 -63
- package/dist/lib/discoveryRoutes.js.map +1 -1
- package/dist/lib/express.d.ts +33 -0
- package/dist/lib/express.js +65 -0
- package/dist/lib/express.js.map +1 -0
- package/dist/lib/getLogger.d.ts +2 -2
- package/dist/lib/getLogger.js.map +1 -1
- package/dist/lib/mcpMessages.d.ts +10 -0
- package/dist/lib/mcpMessages.js +35 -0
- package/dist/lib/mcpMessages.js.map +1 -0
- package/dist/lib/oauthProxy.d.ts +14 -0
- package/dist/lib/oauthProxy.js +80 -0
- package/dist/lib/oauthProxy.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
- package/src/gateways/statefulBridge.ts +87 -167
- package/src/gateways/statelessBridge.ts +100 -218
- package/src/index.ts +2 -2
- package/src/lib/childProcess.ts +120 -0
- package/src/lib/config.ts +9 -0
- package/src/lib/discoveryRoutes.ts +2 -66
- package/src/lib/express.ts +82 -0
- package/src/lib/getLogger.ts +4 -3
- package/src/lib/mcpMessages.ts +42 -0
- package/src/lib/oauthProxy.ts +86 -0
- package/src/types.ts +4 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Express application factory.
|
|
3
|
+
*
|
|
4
|
+
* Creates and configures the common Express setup used by both
|
|
5
|
+
* stateful and stateless bridges.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express, { type Express, type Response } from 'express'
|
|
9
|
+
import cors, { type CorsOptions } from 'cors'
|
|
10
|
+
import type { AuthConfig, Logger } from '../types.js'
|
|
11
|
+
import { serializeCorsOrigin } from './cors.js'
|
|
12
|
+
import { createDiscoveryRouter } from './discoveryRoutes.js'
|
|
13
|
+
import { createOAuthProxyRouter } from './oauthProxy.js'
|
|
14
|
+
import { createAuthMiddleware } from './authMiddleware.js'
|
|
15
|
+
|
|
16
|
+
export interface CreateAppOptions {
|
|
17
|
+
logger: Logger
|
|
18
|
+
corsOrigin: CorsOptions['origin']
|
|
19
|
+
healthEndpoints: string[]
|
|
20
|
+
headers: Record<string, string>
|
|
21
|
+
auth: AuthConfig | null
|
|
22
|
+
/** Path for the MCP endpoint (used to apply auth middleware) */
|
|
23
|
+
mcpPath: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Set custom response headers */
|
|
27
|
+
export const setResponseHeaders = (res: Response, headers: Record<string, string>) =>
|
|
28
|
+
Object.entries(headers).forEach(([key, value]) => res.setHeader(key, value))
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create and configure an Express application with shared middleware.
|
|
32
|
+
*
|
|
33
|
+
* Sets up:
|
|
34
|
+
* - JSON body parser
|
|
35
|
+
* - Trust proxy (for X-Forwarded-* headers)
|
|
36
|
+
* - CORS (if configured)
|
|
37
|
+
* - Health endpoints
|
|
38
|
+
* - OAuth discovery routes (if auth enabled)
|
|
39
|
+
* - OAuth proxy routes (if auth enabled)
|
|
40
|
+
* - Auth middleware on mcpPath (if auth enabled)
|
|
41
|
+
*/
|
|
42
|
+
export function createApp(options: CreateAppOptions): Express {
|
|
43
|
+
const { logger, corsOrigin, healthEndpoints, headers, auth, mcpPath } = options
|
|
44
|
+
|
|
45
|
+
const app = express()
|
|
46
|
+
app.set('trust proxy', true)
|
|
47
|
+
app.use(express.json())
|
|
48
|
+
|
|
49
|
+
// CORS
|
|
50
|
+
if (corsOrigin) {
|
|
51
|
+
app.use(cors({ origin: corsOrigin, exposedHeaders: ['Mcp-Session-Id'] }))
|
|
52
|
+
logger.info(` - CORS: enabled (${serializeCorsOrigin(corsOrigin)})`)
|
|
53
|
+
} else {
|
|
54
|
+
logger.info(' - CORS: disabled')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Health endpoints
|
|
58
|
+
for (const ep of healthEndpoints) {
|
|
59
|
+
app.get(ep, (_req, res) => {
|
|
60
|
+
setResponseHeaders(res, headers)
|
|
61
|
+
res.send('ok')
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
if (healthEndpoints.length) {
|
|
65
|
+
logger.info(` - Health endpoints: ${healthEndpoints.join(', ')}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// OAuth (when auth is enabled)
|
|
69
|
+
if (auth) {
|
|
70
|
+
// Discovery routes (PRM, AS metadata, pseudo-DCR)
|
|
71
|
+
app.use(createDiscoveryRouter(auth, logger))
|
|
72
|
+
// OAuth proxy routes (authorize redirect, token proxy, JWKS proxy)
|
|
73
|
+
app.use(createOAuthProxyRouter(auth, logger))
|
|
74
|
+
// Auth middleware on MCP path
|
|
75
|
+
app.use(mcpPath, createAuthMiddleware(auth, logger))
|
|
76
|
+
logger.info(` - Auth: enabled (issuer=${auth.issuer})`)
|
|
77
|
+
} else {
|
|
78
|
+
logger.info(' - Auth: disabled')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return app
|
|
82
|
+
}
|
package/src/lib/getLogger.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import util from 'node:util'
|
|
2
|
-
import type { Logger } from '../types.js'
|
|
2
|
+
import type { Logger, LogLevel } from '../types.js'
|
|
3
3
|
|
|
4
4
|
const defaultFormatArgs = (args: unknown[]) => args
|
|
5
5
|
|
|
@@ -40,10 +40,11 @@ const debugLogger: Logger = {
|
|
|
40
40
|
error: logStderr({ formatArgs: debugFormatArgs }),
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export type LogLevel = 'none' | 'info' | 'debug'
|
|
44
|
-
|
|
45
43
|
export function getLogger(logLevel: LogLevel): Logger {
|
|
46
44
|
if (logLevel === 'none') return noneLogger
|
|
47
45
|
if (logLevel === 'debug') return debugLogger
|
|
48
46
|
return infoLogger
|
|
49
47
|
}
|
|
48
|
+
|
|
49
|
+
// Re-export LogLevel for backward compatibility
|
|
50
|
+
export type { LogLevel }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP message helpers for auto-initialization and protocol handling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'
|
|
6
|
+
import { VERSION, SERVER_NAME } from './config.js'
|
|
7
|
+
|
|
8
|
+
/** Create a synthetic MCP initialize request */
|
|
9
|
+
export function createInitializeRequest(
|
|
10
|
+
id: string | number,
|
|
11
|
+
protocolVersion: string,
|
|
12
|
+
): JSONRPCMessage {
|
|
13
|
+
return {
|
|
14
|
+
jsonrpc: '2.0',
|
|
15
|
+
id,
|
|
16
|
+
method: 'initialize',
|
|
17
|
+
params: {
|
|
18
|
+
protocolVersion,
|
|
19
|
+
capabilities: {
|
|
20
|
+
roots: { listChanged: true },
|
|
21
|
+
sampling: {},
|
|
22
|
+
},
|
|
23
|
+
clientInfo: {
|
|
24
|
+
name: SERVER_NAME,
|
|
25
|
+
version: VERSION,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Create an initialized notification */
|
|
32
|
+
export function createInitializedNotification(): JSONRPCMessage {
|
|
33
|
+
return {
|
|
34
|
+
jsonrpc: '2.0',
|
|
35
|
+
method: 'notifications/initialized',
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Generate a unique ID for auto-init requests */
|
|
40
|
+
export function generateAutoInitId(): string {
|
|
41
|
+
return `init_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`
|
|
42
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 proxy routes for the bridge.
|
|
3
|
+
*
|
|
4
|
+
* These routes proxy OAuth endpoints to the upstream authorization server:
|
|
5
|
+
* - GET /oauth/authorize → Redirect to upstream (preserves query params)
|
|
6
|
+
* - POST /oauth/token → Proxy to upstream
|
|
7
|
+
* - GET /jwks → Proxy JWKS for token verification
|
|
8
|
+
*
|
|
9
|
+
* This separation allows the bridge to advertise itself as the authorization
|
|
10
|
+
* server while delegating actual auth operations to the upstream issuer.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Router, type Request, type Response } from 'express'
|
|
14
|
+
import type { AuthConfig, Logger } from '../types.js'
|
|
15
|
+
|
|
16
|
+
export function createOAuthProxyRouter(auth: AuthConfig, logger: Logger): Router {
|
|
17
|
+
const router = Router()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OAuth Authorization Endpoint — Redirect to upstream
|
|
21
|
+
*
|
|
22
|
+
* Since the bridge advertises itself as the authorization_server,
|
|
23
|
+
* clients will attempt to call /oauth/authorize here. We redirect
|
|
24
|
+
* to the upstream auth server, preserving all query parameters.
|
|
25
|
+
*/
|
|
26
|
+
router.get('/oauth/authorize', (req: Request, res: Response) => {
|
|
27
|
+
const upstreamUrl = new URL(`${auth.issuer}/oauth/authorize`)
|
|
28
|
+
// Copy all query params to upstream
|
|
29
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
30
|
+
if (typeof value === 'string') {
|
|
31
|
+
upstreamUrl.searchParams.set(key, value)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
logger.info(`[oauth-proxy] Redirecting /oauth/authorize to upstream`)
|
|
35
|
+
res.redirect(upstreamUrl.toString())
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OAuth Token Endpoint — Proxy to upstream
|
|
40
|
+
*
|
|
41
|
+
* Proxies token exchange requests to the upstream auth server.
|
|
42
|
+
*/
|
|
43
|
+
router.post('/oauth/token', async (req: Request, res: Response) => {
|
|
44
|
+
try {
|
|
45
|
+
const upstreamUrl = `${auth.issuer}/oauth/token`
|
|
46
|
+
logger.info('[oauth-proxy] Proxying /oauth/token to upstream')
|
|
47
|
+
|
|
48
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': req.get('Content-Type') || 'application/x-www-form-urlencoded',
|
|
52
|
+
},
|
|
53
|
+
body: req.get('Content-Type')?.includes('application/json')
|
|
54
|
+
? JSON.stringify(req.body)
|
|
55
|
+
: new URLSearchParams(req.body as Record<string, string>).toString(),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const data = await upstreamRes.text()
|
|
59
|
+
res.status(upstreamRes.status)
|
|
60
|
+
res.set('Content-Type', upstreamRes.headers.get('Content-Type') || 'application/json')
|
|
61
|
+
res.send(data)
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
logger.error('[oauth-proxy] Error proxying /oauth/token:', err.message ?? err)
|
|
64
|
+
res.status(502).json({ error: 'upstream_error' })
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* JWKS Endpoint — Proxy to upstream
|
|
70
|
+
*
|
|
71
|
+
* Proxies JSON Web Key Set requests for token verification.
|
|
72
|
+
*/
|
|
73
|
+
router.get('/jwks', async (_req: Request, res: Response) => {
|
|
74
|
+
try {
|
|
75
|
+
const upstreamUrl = `${auth.issuer}/jwks`
|
|
76
|
+
const upstreamRes = await fetch(upstreamUrl)
|
|
77
|
+
const data = await upstreamRes.json()
|
|
78
|
+
res.json(data)
|
|
79
|
+
} catch (err: any) {
|
|
80
|
+
logger.error('[oauth-proxy] Error proxying /jwks:', err.message ?? err)
|
|
81
|
+
res.status(502).json({ error: 'upstream_error' })
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return router
|
|
86
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/** Logger interface for structured logging */
|
|
1
2
|
export interface Logger {
|
|
2
3
|
info: (...args: unknown[]) => void
|
|
3
4
|
error: (...args: unknown[]) => void
|
|
4
5
|
}
|
|
5
6
|
|
|
7
|
+
/** Log level for the logger */
|
|
8
|
+
export type LogLevel = 'none' | 'info' | 'debug'
|
|
9
|
+
|
|
6
10
|
/** OAuth auth config passed when --authIssuer is set */
|
|
7
11
|
export interface AuthConfig {
|
|
8
12
|
/** OAuth issuer URL (e.g. https://auth.platf.ai) */
|