@platf/bridge 0.0.2
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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/gateways/statefulBridge.d.ts +22 -0
- package/dist/gateways/statefulBridge.js +211 -0
- package/dist/gateways/statefulBridge.js.map +1 -0
- package/dist/gateways/statelessBridge.d.ts +22 -0
- package/dist/gateways/statelessBridge.js +234 -0
- package/dist/gateways/statelessBridge.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/authMiddleware.d.ts +12 -0
- package/dist/lib/authMiddleware.js +41 -0
- package/dist/lib/authMiddleware.js.map +1 -0
- package/dist/lib/cors.d.ts +3 -0
- package/dist/lib/cors.js +29 -0
- package/dist/lib/cors.js.map +1 -0
- package/dist/lib/discoveryRoutes.d.ts +14 -0
- package/dist/lib/discoveryRoutes.js +86 -0
- package/dist/lib/discoveryRoutes.js.map +1 -0
- package/dist/lib/getLogger.d.ts +3 -0
- package/dist/lib/getLogger.js +34 -0
- package/dist/lib/getLogger.js.map +1 -0
- package/dist/lib/headers.d.ts +2 -0
- package/dist/lib/headers.js +19 -0
- package/dist/lib/headers.js.map +1 -0
- package/dist/lib/onSignals.d.ts +6 -0
- package/dist/lib/onSignals.js +16 -0
- package/dist/lib/onSignals.js.map +1 -0
- package/dist/lib/sessionAccessCounter.d.ts +11 -0
- package/dist/lib/sessionAccessCounter.js +72 -0
- package/dist/lib/sessionAccessCounter.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
- package/src/gateways/statefulBridge.ts +271 -0
- package/src/gateways/statelessBridge.ts +310 -0
- package/src/index.ts +121 -0
- package/src/lib/authMiddleware.ts +58 -0
- package/src/lib/cors.ts +30 -0
- package/src/lib/discoveryRoutes.ts +95 -0
- package/src/lib/getLogger.ts +49 -0
- package/src/lib/headers.ts +26 -0
- package/src/lib/onSignals.ts +24 -0
- package/src/lib/sessionAccessCounter.ts +98 -0
- package/src/types.ts +12 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
import { getLogger } from './lib/getLogger.js';
|
|
5
|
+
import { parseCorsOrigin } from './lib/cors.js';
|
|
6
|
+
import { parseHeaders } from './lib/headers.js';
|
|
7
|
+
import { startStatelessBridge } from './gateways/statelessBridge.js';
|
|
8
|
+
import { startStatefulBridge } from './gateways/statefulBridge.js';
|
|
9
|
+
const argv = await yargs(hideBin(process.argv))
|
|
10
|
+
.scriptName('platf-bridge')
|
|
11
|
+
.usage('$0 — Stdio-to-Streamable HTTP bridge for MCP servers')
|
|
12
|
+
.option('stdio', {
|
|
13
|
+
type: 'string',
|
|
14
|
+
demandOption: true,
|
|
15
|
+
describe: 'Shell command that speaks MCP over stdio (stdin/stdout)',
|
|
16
|
+
})
|
|
17
|
+
.option('port', {
|
|
18
|
+
type: 'number',
|
|
19
|
+
default: 8000,
|
|
20
|
+
describe: 'HTTP port to listen on',
|
|
21
|
+
})
|
|
22
|
+
.option('path', {
|
|
23
|
+
type: 'string',
|
|
24
|
+
default: '/mcp',
|
|
25
|
+
describe: 'HTTP path for the Streamable HTTP endpoint',
|
|
26
|
+
})
|
|
27
|
+
.option('stateful', {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
default: false,
|
|
30
|
+
describe: 'Enable stateful mode (session-based, persistent child process per session)',
|
|
31
|
+
})
|
|
32
|
+
.option('sessionTimeout', {
|
|
33
|
+
type: 'number',
|
|
34
|
+
describe: 'Session inactivity timeout in milliseconds (stateful mode only)',
|
|
35
|
+
})
|
|
36
|
+
.option('protocolVersion', {
|
|
37
|
+
type: 'string',
|
|
38
|
+
default: '2025-03-26',
|
|
39
|
+
describe: 'MCP protocol version for auto-initialization (stateless mode)',
|
|
40
|
+
})
|
|
41
|
+
.option('logLevel', {
|
|
42
|
+
type: 'string',
|
|
43
|
+
choices: ['none', 'info', 'debug'],
|
|
44
|
+
default: 'info',
|
|
45
|
+
describe: 'Log verbosity',
|
|
46
|
+
})
|
|
47
|
+
.option('cors', {
|
|
48
|
+
type: 'array',
|
|
49
|
+
describe: 'CORS origins to allow (omit for no CORS, pass * for all)',
|
|
50
|
+
})
|
|
51
|
+
.option('healthEndpoint', {
|
|
52
|
+
type: 'array',
|
|
53
|
+
string: true,
|
|
54
|
+
default: [],
|
|
55
|
+
describe: 'Path(s) that return 200 "ok" for health checks',
|
|
56
|
+
})
|
|
57
|
+
.option('header', {
|
|
58
|
+
type: 'array',
|
|
59
|
+
default: [],
|
|
60
|
+
describe: 'Additional response headers in "Key: Value" format',
|
|
61
|
+
})
|
|
62
|
+
.option('authIssuer', {
|
|
63
|
+
type: 'string',
|
|
64
|
+
describe: 'OAuth issuer URL (e.g. https://auth.platf.ai). Enables auth when set.',
|
|
65
|
+
})
|
|
66
|
+
.option('authClientId', {
|
|
67
|
+
type: 'string',
|
|
68
|
+
describe: 'OAuth client_id for this bridge instance (pre-registered with auth issuer)',
|
|
69
|
+
})
|
|
70
|
+
.strict()
|
|
71
|
+
.help()
|
|
72
|
+
.parse();
|
|
73
|
+
const logger = getLogger(argv.logLevel);
|
|
74
|
+
const corsOrigin = parseCorsOrigin(argv.cors);
|
|
75
|
+
const headers = parseHeaders(argv.header, logger);
|
|
76
|
+
// Build auth config (only when --authIssuer is provided)
|
|
77
|
+
let auth = null;
|
|
78
|
+
if (argv.authIssuer) {
|
|
79
|
+
if (!argv.authClientId) {
|
|
80
|
+
console.error('Error: --authClientId is required when --authIssuer is set');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
auth = { issuer: argv.authIssuer.replace(/\/$/, ''), clientId: argv.authClientId };
|
|
84
|
+
logger.info(` Auth: enabled (issuer=${auth.issuer}, clientId=${auth.clientId})`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.info(' Auth: disabled');
|
|
88
|
+
}
|
|
89
|
+
logger.info('platf-bridge starting...');
|
|
90
|
+
logger.info(` Mode: ${argv.stateful ? 'stateful' : 'stateless'}`);
|
|
91
|
+
if (argv.stateful) {
|
|
92
|
+
await startStatefulBridge({
|
|
93
|
+
stdioCmd: argv.stdio,
|
|
94
|
+
port: argv.port,
|
|
95
|
+
path: argv.path,
|
|
96
|
+
logger,
|
|
97
|
+
corsOrigin,
|
|
98
|
+
healthEndpoints: argv.healthEndpoint,
|
|
99
|
+
headers,
|
|
100
|
+
sessionTimeout: argv.sessionTimeout ?? null,
|
|
101
|
+
auth,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
await startStatelessBridge({
|
|
106
|
+
stdioCmd: argv.stdio,
|
|
107
|
+
port: argv.port,
|
|
108
|
+
path: argv.path,
|
|
109
|
+
logger,
|
|
110
|
+
corsOrigin,
|
|
111
|
+
healthEndpoints: argv.healthEndpoint,
|
|
112
|
+
headers,
|
|
113
|
+
protocolVersion: argv.protocolVersion,
|
|
114
|
+
auth,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 JWT auth middleware for the bridge.
|
|
3
|
+
*
|
|
4
|
+
* - Fetches JWKS from `{issuer}/jwks` using jose's createRemoteJWKSet (auto-caches).
|
|
5
|
+
* - Validates Bearer token: signature, `iss`, `exp`.
|
|
6
|
+
* - On failure returns 401 with RFC 9728–compliant `WWW-Authenticate` header
|
|
7
|
+
* pointing at the OAuth Protected Resource Metadata document.
|
|
8
|
+
*/
|
|
9
|
+
import type { RequestHandler } from 'express';
|
|
10
|
+
import type { AuthConfig, Logger } from '../types.js';
|
|
11
|
+
/** Build a reusable auth middleware for the given auth config */
|
|
12
|
+
export declare function createAuthMiddleware(auth: AuthConfig, logger: Logger): RequestHandler;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 JWT auth middleware for the bridge.
|
|
3
|
+
*
|
|
4
|
+
* - Fetches JWKS from `{issuer}/jwks` using jose's createRemoteJWKSet (auto-caches).
|
|
5
|
+
* - Validates Bearer token: signature, `iss`, `exp`.
|
|
6
|
+
* - On failure returns 401 with RFC 9728–compliant `WWW-Authenticate` header
|
|
7
|
+
* pointing at the OAuth Protected Resource Metadata document.
|
|
8
|
+
*/
|
|
9
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
10
|
+
/** Build a reusable auth middleware for the given auth config */
|
|
11
|
+
export function createAuthMiddleware(auth, logger) {
|
|
12
|
+
const jwksUri = new URL('/jwks', auth.issuer);
|
|
13
|
+
const JWKS = createRemoteJWKSet(jwksUri);
|
|
14
|
+
return async (req, res, next) => {
|
|
15
|
+
const authHeader = req.headers.authorization;
|
|
16
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
17
|
+
return unauthorized(req, res, auth, 'missing or malformed Authorization header');
|
|
18
|
+
}
|
|
19
|
+
const token = authHeader.slice(7);
|
|
20
|
+
try {
|
|
21
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
22
|
+
issuer: auth.issuer,
|
|
23
|
+
});
|
|
24
|
+
req.tokenPayload = payload;
|
|
25
|
+
next();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
logger.error('[auth] JWT verification failed:', err.message ?? err);
|
|
29
|
+
return unauthorized(req, res, auth, 'invalid_token');
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function unauthorized(req, res, auth, error) {
|
|
34
|
+
// RFC 9728: include resource_metadata URI in WWW-Authenticate
|
|
35
|
+
const scheme = req.protocol;
|
|
36
|
+
const host = req.get('host');
|
|
37
|
+
const resourceMetadataUri = `${scheme}://${host}/.well-known/oauth-protected-resource`;
|
|
38
|
+
res.setHeader('WWW-Authenticate', `Bearer realm="mcp", error="${error}", resource_metadata="${resourceMetadataUri}"`);
|
|
39
|
+
res.status(401).json({ error: 'unauthorized', message: error });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=authMiddleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/lib/authMiddleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAA;AAIpD,iEAAiE;AACjE,MAAM,UAAU,oBAAoB,CAAC,IAAgB,EAAE,MAAc;IACnE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAC7C,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAExC,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC9B,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAA;QAC5C,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACrD,OAAO,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,2CAA2C,CAAC,CAAA;QAClF,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAEjC,IAAI,CAAC;YACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;gBAC/C,MAAM,EAAE,IAAI,CAAC,MAAM;aACpB,CAAC,CAGD;YAAC,GAAW,CAAC,YAAY,GAAG,OAAO,CAAA;YACpC,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,KAAK,CAAC,iCAAiC,EAAE,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,CAAA;YACnE,OAAO,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,eAAe,CAAC,CAAA;QACtD,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CACnB,GAA8B,EAC9B,GAA+B,EAC/B,IAAgB,EAChB,KAAa;IAEb,8DAA8D;IAC9D,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAA;IAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC5B,MAAM,mBAAmB,GAAG,GAAG,MAAM,MAAM,IAAI,uCAAuC,CAAA;IAEtF,GAAG,CAAC,SAAS,CACX,kBAAkB,EAClB,8BAA8B,KAAK,yBAAyB,mBAAmB,GAAG,CACnF,CAAA;IACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;AACjE,CAAC"}
|
package/dist/lib/cors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function parseCorsOrigin(corsValues) {
|
|
2
|
+
if (!corsValues)
|
|
3
|
+
return false;
|
|
4
|
+
if (corsValues.length === 0)
|
|
5
|
+
return '*';
|
|
6
|
+
const origins = corsValues.map((item) => `${item}`);
|
|
7
|
+
if (origins.includes('*'))
|
|
8
|
+
return '*';
|
|
9
|
+
return origins.map((origin) => {
|
|
10
|
+
if (/^\/.*\/$/.test(origin)) {
|
|
11
|
+
const pattern = origin.slice(1, -1);
|
|
12
|
+
try {
|
|
13
|
+
return new RegExp(pattern);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return origin;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return origin;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function serializeCorsOrigin(corsOrigin) {
|
|
23
|
+
return JSON.stringify(corsOrigin, (_key, value) => {
|
|
24
|
+
if (value instanceof RegExp)
|
|
25
|
+
return value.toString();
|
|
26
|
+
return value;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=cors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.js","sourceRoot":"","sources":["../../src/lib/cors.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,eAAe,CAC7B,UAA2C;IAE3C,IAAI,CAAC,UAAU;QAAE,OAAO,KAAK,CAAA;IAC7B,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAA;IAEvC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,CAAA;IACnD,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAA;IAErC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC5B,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YACnC,IAAI,CAAC;gBACH,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAA;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,MAAM,CAAA;YACf,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAiC;IACnE,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAChD,IAAI,KAAK,YAAY,MAAM;YAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;QACpD,OAAO,KAAK,CAAA;IACd,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 discovery endpoints for the bridge.
|
|
3
|
+
*
|
|
4
|
+
* When auth is enabled these routes expose:
|
|
5
|
+
* - GET /.well-known/oauth-protected-resource[/*] (RFC 9728)
|
|
6
|
+
* - GET /.well-known/oauth-authorization-server[/*] (RFC 8414 — proxied from issuer)
|
|
7
|
+
* - POST /oauth/register (Pseudo-DCR — RFC 7591)
|
|
8
|
+
*
|
|
9
|
+
* These endpoints are unauthenticated — they must be accessible to
|
|
10
|
+
* any client performing OAuth discovery before obtaining a token.
|
|
11
|
+
*/
|
|
12
|
+
import { Router } from 'express';
|
|
13
|
+
import type { AuthConfig, Logger } from '../types.js';
|
|
14
|
+
export declare function createDiscoveryRouter(auth: AuthConfig, logger: Logger): Router;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 discovery endpoints for the bridge.
|
|
3
|
+
*
|
|
4
|
+
* When auth is enabled these routes expose:
|
|
5
|
+
* - GET /.well-known/oauth-protected-resource[/*] (RFC 9728)
|
|
6
|
+
* - GET /.well-known/oauth-authorization-server[/*] (RFC 8414 — proxied from issuer)
|
|
7
|
+
* - POST /oauth/register (Pseudo-DCR — RFC 7591)
|
|
8
|
+
*
|
|
9
|
+
* These endpoints are unauthenticated — they must be accessible to
|
|
10
|
+
* any client performing OAuth discovery before obtaining a token.
|
|
11
|
+
*/
|
|
12
|
+
import { Router } from 'express';
|
|
13
|
+
export function createDiscoveryRouter(auth, logger) {
|
|
14
|
+
const router = Router();
|
|
15
|
+
/**
|
|
16
|
+
* RFC 9728 — OAuth Protected Resource Metadata
|
|
17
|
+
*
|
|
18
|
+
* Tells the client:
|
|
19
|
+
* - which authorization server protects this resource
|
|
20
|
+
* - which scopes are available
|
|
21
|
+
* - where to find the authorization server metadata
|
|
22
|
+
*
|
|
23
|
+
* Handles both root and path-suffixed variants (e.g. /.well-known/oauth-protected-resource/mcp)
|
|
24
|
+
* as required by the MCP SDK for path-based resource discovery.
|
|
25
|
+
*/
|
|
26
|
+
router.get('/.well-known/oauth-protected-resource*', (req, res) => {
|
|
27
|
+
const scheme = req.protocol;
|
|
28
|
+
const host = req.get('host');
|
|
29
|
+
const resourceMetadata = {
|
|
30
|
+
resource: `${scheme}://${host}/`,
|
|
31
|
+
authorization_servers: [auth.issuer],
|
|
32
|
+
scopes_supported: ['openid', 'profile', 'email'],
|
|
33
|
+
bearer_methods_supported: ['header'],
|
|
34
|
+
};
|
|
35
|
+
res.json(resourceMetadata);
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* RFC 8414 — Authorization Server Metadata (proxied)
|
|
39
|
+
*
|
|
40
|
+
* We proxy the issuer's .well-known/oauth-authorization-server document
|
|
41
|
+
* and patch `registration_endpoint` to point to our local pseudo-DCR.
|
|
42
|
+
*
|
|
43
|
+
* Handles both root and path-suffixed variants.
|
|
44
|
+
*/
|
|
45
|
+
router.get('/.well-known/oauth-authorization-server*', async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const metadataUrl = `${auth.issuer}/.well-known/oauth-authorization-server`;
|
|
48
|
+
const upstream = await fetch(metadataUrl);
|
|
49
|
+
if (!upstream.ok) {
|
|
50
|
+
logger.error(`[discovery] Failed to fetch AS metadata: ${upstream.status}`);
|
|
51
|
+
return res.status(502).json({ error: 'upstream_error' });
|
|
52
|
+
}
|
|
53
|
+
const metadata = (await upstream.json());
|
|
54
|
+
// Patch registration_endpoint to point to our pseudo-DCR
|
|
55
|
+
const scheme = req.protocol;
|
|
56
|
+
const host = req.get('host');
|
|
57
|
+
metadata.registration_endpoint = `${scheme}://${host}/oauth/register`;
|
|
58
|
+
res.json(metadata);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger.error('[discovery] Error proxying AS metadata:', err.message ?? err);
|
|
62
|
+
res.status(502).json({ error: 'upstream_error' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Pseudo–Dynamic Client Registration (RFC 7591)
|
|
67
|
+
*
|
|
68
|
+
* Always returns the same pre-registered client_id — no new client
|
|
69
|
+
* is actually created. This lets standards-based OAuth clients
|
|
70
|
+
* (e.g., VS Code Copilot) discover the correct client_id through
|
|
71
|
+
* the normal DCR flow without requiring out-of-band configuration.
|
|
72
|
+
*/
|
|
73
|
+
router.post('/oauth/register', (_req, res) => {
|
|
74
|
+
res.status(201).json({
|
|
75
|
+
client_id: auth.clientId,
|
|
76
|
+
client_name: 'platf-bridge',
|
|
77
|
+
// No client_secret — public client using PKCE
|
|
78
|
+
token_endpoint_auth_method: 'none',
|
|
79
|
+
grant_types: ['authorization_code'],
|
|
80
|
+
response_types: ['code'],
|
|
81
|
+
redirect_uris: [], // Client provides its own redirect_uri
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return router;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=discoveryRoutes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discoveryRoutes.js","sourceRoot":"","sources":["../../src/lib/discoveryRoutes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,MAAM,EAA+B,MAAM,SAAS,CAAA;AAG7D,MAAM,UAAU,qBAAqB,CAAC,IAAgB,EAAE,MAAc;IACpE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAA;IAEvB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,GAAG,CAAC,wCAAwC,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QACnF,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAA;QAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QAC5B,MAAM,gBAAgB,GAAG;YACvB,QAAQ,EAAE,GAAG,MAAM,MAAM,IAAI,GAAG;YAChC,qBAAqB,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC;YACpC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;YAChD,wBAAwB,EAAE,CAAC,QAAQ,CAAC;SACrC,CAAA;QACD,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF;;;;;;;OAOG;IACH,MAAM,CAAC,GAAG,CAAC,0CAA0C,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QAC3F,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,GAAG,IAAI,CAAC,MAAM,yCAAyC,CAAA;YAC3E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,CAAA;YAEzC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,CAAC,KAAK,CAAC,4CAA4C,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;gBAC3E,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAA;YAC1D,CAAC;YAED,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA4B,CAAA;YAEnE,yDAAyD;YACzD,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAA;YAC3B,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC5B,QAAQ,CAAC,qBAAqB,GAAG,GAAG,MAAM,MAAM,IAAI,iBAAiB,CAAA;YAErE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACpB,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,CAAA;YAC3E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAA;QACnD,CAAC;IACH,CAAC,CAAC,CAAA;IAEF;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;QAC9D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,SAAS,EAAE,IAAI,CAAC,QAAQ;YACxB,WAAW,EAAE,cAAc;YAC3B,8CAA8C;YAC9C,0BAA0B,EAAE,MAAM;YAClC,WAAW,EAAE,CAAC,oBAAoB,CAAC;YACnC,cAAc,EAAE,CAAC,MAAM,CAAC;YACxB,aAAa,EAAE,EAAE,EAAE,uCAAuC;SAC3D,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import util from 'node:util';
|
|
2
|
+
const defaultFormatArgs = (args) => args;
|
|
3
|
+
const log = ({ formatArgs = defaultFormatArgs } = {}) => (...args) => console.log('[platf-bridge]', ...formatArgs(args));
|
|
4
|
+
const logStderr = ({ formatArgs = defaultFormatArgs } = {}) => (...args) => console.error('[platf-bridge]', ...formatArgs(args));
|
|
5
|
+
const noneLogger = {
|
|
6
|
+
info: () => { },
|
|
7
|
+
error: () => { },
|
|
8
|
+
};
|
|
9
|
+
const infoLogger = {
|
|
10
|
+
info: log(),
|
|
11
|
+
error: logStderr(),
|
|
12
|
+
};
|
|
13
|
+
const debugFormatArgs = (args) => args.map((arg) => {
|
|
14
|
+
if (typeof arg === 'object') {
|
|
15
|
+
return util.inspect(arg, {
|
|
16
|
+
depth: null,
|
|
17
|
+
colors: process.stderr.isTTY,
|
|
18
|
+
compact: false,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return arg;
|
|
22
|
+
});
|
|
23
|
+
const debugLogger = {
|
|
24
|
+
info: log({ formatArgs: debugFormatArgs }),
|
|
25
|
+
error: logStderr({ formatArgs: debugFormatArgs }),
|
|
26
|
+
};
|
|
27
|
+
export function getLogger(logLevel) {
|
|
28
|
+
if (logLevel === 'none')
|
|
29
|
+
return noneLogger;
|
|
30
|
+
if (logLevel === 'debug')
|
|
31
|
+
return debugLogger;
|
|
32
|
+
return infoLogger;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=getLogger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getLogger.js","sourceRoot":"","sources":["../../src/lib/getLogger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B,MAAM,iBAAiB,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,IAAI,CAAA;AAEnD,MAAM,GAAG,GACP,CAAC,EAAE,UAAU,GAAG,iBAAiB,KAAgD,EAAE,EAAE,EAAE,CACvF,CAAC,GAAG,IAAe,EAAE,EAAE,CACrB,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;AAEtD,MAAM,SAAS,GACb,CAAC,EAAE,UAAU,GAAG,iBAAiB,KAAgD,EAAE,EAAE,EAAE,CACvF,CAAC,GAAG,IAAe,EAAE,EAAE,CACrB,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;AAExD,MAAM,UAAU,GAAW;IACzB,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;CAChB,CAAA;AAED,MAAM,UAAU,GAAW;IACzB,IAAI,EAAE,GAAG,EAAE;IACX,KAAK,EAAE,SAAS,EAAE;CACnB,CAAA;AAED,MAAM,eAAe,GAAG,CAAC,IAAe,EAAE,EAAE,CAC1C,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;IACf,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YACvB,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK;YAC5B,OAAO,EAAE,KAAK;SACf,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAC,CAAA;AAEJ,MAAM,WAAW,GAAW;IAC1B,IAAI,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;IAC1C,KAAK,EAAE,SAAS,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;CAClD,CAAA;AAID,MAAM,UAAU,SAAS,CAAC,QAAkB;IAC1C,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,UAAU,CAAA;IAC1C,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,WAAW,CAAA;IAC5C,OAAO,UAAU,CAAA;AACnB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function parseHeaders(rawHeaders, logger) {
|
|
2
|
+
return rawHeaders.reduce((acc, rawHeader) => {
|
|
3
|
+
const header = `${rawHeader}`;
|
|
4
|
+
const colonIndex = header.indexOf(':');
|
|
5
|
+
if (colonIndex === -1) {
|
|
6
|
+
logger.error(`Invalid header format: ${header}, ignoring`);
|
|
7
|
+
return acc;
|
|
8
|
+
}
|
|
9
|
+
const key = header.slice(0, colonIndex).trim();
|
|
10
|
+
const value = header.slice(colonIndex + 1).trim();
|
|
11
|
+
if (!key || !value) {
|
|
12
|
+
logger.error(`Invalid header format: ${header}, ignoring`);
|
|
13
|
+
return acc;
|
|
14
|
+
}
|
|
15
|
+
acc[key] = value;
|
|
16
|
+
return acc;
|
|
17
|
+
}, {});
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/lib/headers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAC1B,UAA+B,EAC/B,MAAc;IAEd,OAAO,UAAU,CAAC,MAAM,CAAyB,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE;QAClE,MAAM,MAAM,GAAG,GAAG,SAAS,EAAE,CAAA;QAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,0BAA0B,MAAM,YAAY,CAAC,CAAA;YAC1D,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAA;QAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAEjD,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,CAAC,KAAK,CAAC,0BAA0B,MAAM,YAAY,CAAC,CAAA;YAC1D,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QAChB,OAAO,GAAG,CAAA;IACZ,CAAC,EAAE,EAAE,CAAC,CAAA;AACR,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function onSignals({ logger, cleanup }) {
|
|
2
|
+
const handleSignal = (signal) => {
|
|
3
|
+
logger.info(`Caught ${signal}. Exiting...`);
|
|
4
|
+
cleanup?.();
|
|
5
|
+
process.exit(0);
|
|
6
|
+
};
|
|
7
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
8
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
9
|
+
process.on('SIGHUP', () => handleSignal('SIGHUP'));
|
|
10
|
+
process.stdin.on('close', () => {
|
|
11
|
+
logger.info('stdin closed. Exiting...');
|
|
12
|
+
cleanup?.();
|
|
13
|
+
process.exit(0);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=onSignals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onSignals.js","sourceRoot":"","sources":["../../src/lib/onSignals.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAoB;IAC7D,MAAM,YAAY,GAAG,CAAC,MAAc,EAAE,EAAE;QACtC,MAAM,CAAC,IAAI,CAAC,UAAU,MAAM,cAAc,CAAC,CAAA;QAC3C,OAAO,EAAE,EAAE,CAAA;QACX,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAA;IAED,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAClD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAA;IACpD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAElD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QACvC,OAAO,EAAE,EAAE,CAAA;QACX,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Logger } from '../types.js';
|
|
2
|
+
export declare class SessionAccessCounter {
|
|
3
|
+
private readonly timeoutMs;
|
|
4
|
+
private readonly cleanup;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
private sessions;
|
|
7
|
+
constructor(timeoutMs: number, cleanup: (sessionId: string) => unknown, logger: Logger);
|
|
8
|
+
inc(sessionId: string, reason: string): void;
|
|
9
|
+
dec(sessionId: string, reason: string): void;
|
|
10
|
+
clear(sessionId: string, runCleanup: boolean, reason: string): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export class SessionAccessCounter {
|
|
2
|
+
timeoutMs;
|
|
3
|
+
cleanup;
|
|
4
|
+
logger;
|
|
5
|
+
sessions = new Map();
|
|
6
|
+
constructor(timeoutMs, cleanup, logger) {
|
|
7
|
+
this.timeoutMs = timeoutMs;
|
|
8
|
+
this.cleanup = cleanup;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
}
|
|
11
|
+
inc(sessionId, reason) {
|
|
12
|
+
this.logger.info(`SessionAccessCounter.inc() ${sessionId}, caused by ${reason}`);
|
|
13
|
+
const session = this.sessions.get(sessionId);
|
|
14
|
+
if (!session) {
|
|
15
|
+
this.logger.info(`Session access count 0 -> 1 for ${sessionId} (new session)`);
|
|
16
|
+
this.sessions.set(sessionId, { accessCount: 1 });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if ('timeout' in session) {
|
|
20
|
+
this.logger.info(`Session access count 0 -> 1, clearing cleanup timeout for ${sessionId}`);
|
|
21
|
+
clearTimeout(session.timeout);
|
|
22
|
+
this.sessions.set(sessionId, { accessCount: 1 });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
this.logger.info(`Session access count ${session.accessCount} -> ${session.accessCount + 1} for ${sessionId}`);
|
|
26
|
+
session.accessCount++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
dec(sessionId, reason) {
|
|
30
|
+
this.logger.info(`SessionAccessCounter.dec() ${sessionId}, caused by ${reason}`);
|
|
31
|
+
const session = this.sessions.get(sessionId);
|
|
32
|
+
if (!session) {
|
|
33
|
+
this.logger.error(`Called dec() on non-existent session ${sessionId}, ignoring`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if ('timeout' in session) {
|
|
37
|
+
this.logger.error(`Called dec() on session ${sessionId} that is already pending cleanup, ignoring`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (session.accessCount <= 0) {
|
|
41
|
+
throw new Error(`Invalid access count ${session.accessCount} for session ${sessionId}`);
|
|
42
|
+
}
|
|
43
|
+
session.accessCount--;
|
|
44
|
+
this.logger.info(`Session access count ${session.accessCount + 1} -> ${session.accessCount} for ${sessionId}`);
|
|
45
|
+
if (session.accessCount === 0) {
|
|
46
|
+
this.logger.info(`Session access count reached 0, setting cleanup timeout for ${sessionId}`);
|
|
47
|
+
this.sessions.set(sessionId, {
|
|
48
|
+
timeout: setTimeout(() => {
|
|
49
|
+
this.logger.info(`Session ${sessionId} timed out, cleaning up`);
|
|
50
|
+
this.sessions.delete(sessionId);
|
|
51
|
+
this.cleanup(sessionId);
|
|
52
|
+
}, this.timeoutMs),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
clear(sessionId, runCleanup, reason) {
|
|
57
|
+
this.logger.info(`SessionAccessCounter.clear() ${sessionId}, caused by ${reason}`);
|
|
58
|
+
const session = this.sessions.get(sessionId);
|
|
59
|
+
if (!session) {
|
|
60
|
+
this.logger.info(`Attempted to clear non-existent session ${sessionId}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if ('timeout' in session) {
|
|
64
|
+
clearTimeout(session.timeout);
|
|
65
|
+
}
|
|
66
|
+
this.sessions.delete(sessionId);
|
|
67
|
+
if (runCleanup) {
|
|
68
|
+
this.cleanup(sessionId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=sessionAccessCounter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionAccessCounter.js","sourceRoot":"","sources":["../../src/lib/sessionAccessCounter.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Logger {
|
|
2
|
+
info: (...args: unknown[]) => void;
|
|
3
|
+
error: (...args: unknown[]) => void;
|
|
4
|
+
}
|
|
5
|
+
/** OAuth auth config passed when --authIssuer is set */
|
|
6
|
+
export interface AuthConfig {
|
|
7
|
+
/** OAuth issuer URL (e.g. https://auth.platf.ai) */
|
|
8
|
+
issuer: string;
|
|
9
|
+
/** OAuth client_id for this bridge instance */
|
|
10
|
+
clientId: string;
|
|
11
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platf/bridge",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Stdio-to-Streamable HTTP bridge for MCP servers — Platf AI Hub",
|
|
5
|
+
"module": "src/index.ts",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": "dist/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "bun run src/index.ts",
|
|
16
|
+
"dev": "bun run --watch src/index.ts",
|
|
17
|
+
"build": "tsc -p tsconfig.build.json",
|
|
18
|
+
"prepublishOnly": "tsc -p tsconfig.build.json",
|
|
19
|
+
"typecheck": "bunx tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.18.2",
|
|
23
|
+
"cors": "^2.8.5",
|
|
24
|
+
"express": "^4.21.2",
|
|
25
|
+
"jose": "^6.1.3",
|
|
26
|
+
"yargs": "^17.7.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/bun": "latest",
|
|
30
|
+
"@types/cors": "^2.8.17",
|
|
31
|
+
"@types/express": "^5.0.3",
|
|
32
|
+
"@types/yargs": "^17.0.33"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18",
|
|
39
|
+
"bun": ">=1"
|
|
40
|
+
}
|
|
41
|
+
}
|