@kwonye/mcpx 0.1.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/adapters/claude.d.ts +7 -0
  4. package/dist/adapters/claude.js +69 -0
  5. package/dist/adapters/claude.js.map +1 -0
  6. package/dist/adapters/cline.d.ts +7 -0
  7. package/dist/adapters/cline.js +65 -0
  8. package/dist/adapters/cline.js.map +1 -0
  9. package/dist/adapters/codex.d.ts +7 -0
  10. package/dist/adapters/codex.js +52 -0
  11. package/dist/adapters/codex.js.map +1 -0
  12. package/dist/adapters/cursor.d.ts +7 -0
  13. package/dist/adapters/cursor.js +52 -0
  14. package/dist/adapters/cursor.js.map +1 -0
  15. package/dist/adapters/index.d.ts +2 -0
  16. package/dist/adapters/index.js +15 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/adapters/utils.d.ts +10 -0
  19. package/dist/adapters/utils.js +69 -0
  20. package/dist/adapters/utils.js.map +1 -0
  21. package/dist/adapters/vscode.d.ts +7 -0
  22. package/dist/adapters/vscode.js +52 -0
  23. package/dist/adapters/vscode.js.map +1 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +577 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/core/config.d.ts +4 -0
  28. package/dist/core/config.js +71 -0
  29. package/dist/core/config.js.map +1 -0
  30. package/dist/core/daemon.d.ts +25 -0
  31. package/dist/core/daemon.js +174 -0
  32. package/dist/core/daemon.js.map +1 -0
  33. package/dist/core/managed-index.d.ts +4 -0
  34. package/dist/core/managed-index.js +22 -0
  35. package/dist/core/managed-index.js.map +1 -0
  36. package/dist/core/paths.d.ts +12 -0
  37. package/dist/core/paths.js +46 -0
  38. package/dist/core/paths.js.map +1 -0
  39. package/dist/core/registry.d.ts +7 -0
  40. package/dist/core/registry.js +33 -0
  41. package/dist/core/registry.js.map +1 -0
  42. package/dist/core/secrets.d.ts +16 -0
  43. package/dist/core/secrets.js +108 -0
  44. package/dist/core/secrets.js.map +1 -0
  45. package/dist/core/server-auth.d.ts +19 -0
  46. package/dist/core/server-auth.js +112 -0
  47. package/dist/core/server-auth.js.map +1 -0
  48. package/dist/core/sync.d.ts +9 -0
  49. package/dist/core/sync.js +63 -0
  50. package/dist/core/sync.js.map +1 -0
  51. package/dist/gateway/server.d.ts +9 -0
  52. package/dist/gateway/server.js +960 -0
  53. package/dist/gateway/server.js.map +1 -0
  54. package/dist/types.d.ts +86 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/util/fs.d.ts +4 -0
  58. package/dist/util/fs.js +34 -0
  59. package/dist/util/fs.js.map +1 -0
  60. package/dist/version.d.ts +1 -0
  61. package/dist/version.js +2 -0
  62. package/dist/version.js.map +1 -0
  63. package/package.json +60 -0
@@ -0,0 +1,960 @@
1
+ import http from "node:http";
2
+ import { URL } from "node:url";
3
+ import crypto from "node:crypto";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
6
+ import { loadConfig } from "../core/config.js";
7
+ import { APP_VERSION } from "../version.js";
8
+ const JSON_RPC_VERSION = "2.0";
9
+ const SERVER_VERSION = APP_VERSION;
10
+ const DEFAULT_UPSTREAM_TIMEOUT_MS = 30_000;
11
+ const OAUTH_WELL_KNOWN_PREFIXES = [
12
+ "/.well-known/oauth-protected-resource",
13
+ "/.well-known/oauth-authorization-server",
14
+ "/.well-known/openid-configuration"
15
+ ];
16
+ class UpstreamHttpError extends Error {
17
+ status;
18
+ wwwAuthenticate;
19
+ bodyText;
20
+ constructor(upstreamName, status, bodyText, wwwAuthenticate) {
21
+ super(`Upstream ${upstreamName} returned HTTP ${status}: ${bodyText.slice(0, 400)}`);
22
+ this.name = "UpstreamHttpError";
23
+ this.status = status;
24
+ this.bodyText = bodyText;
25
+ this.wwwAuthenticate = wwwAuthenticate;
26
+ }
27
+ }
28
+ function makeError(id, code, message, data) {
29
+ return {
30
+ jsonrpc: "2.0",
31
+ id,
32
+ error: {
33
+ code,
34
+ message,
35
+ data
36
+ }
37
+ };
38
+ }
39
+ function makeResult(id, result) {
40
+ return {
41
+ jsonrpc: "2.0",
42
+ id,
43
+ result
44
+ };
45
+ }
46
+ function listUpstreams(config, upstreamFilter) {
47
+ const all = Object.entries(config.servers).map(([name, spec]) => ({ name, spec }));
48
+ if (!upstreamFilter) {
49
+ return all;
50
+ }
51
+ return all.filter((upstream) => upstream.name === upstreamFilter);
52
+ }
53
+ function getSingleUpstream(config) {
54
+ const upstreams = listUpstreams(config);
55
+ if (upstreams.length !== 1) {
56
+ return null;
57
+ }
58
+ return upstreams[0] ?? null;
59
+ }
60
+ function getSingleHttpUpstream(config) {
61
+ const upstream = getSingleUpstream(config);
62
+ if (!upstream || upstream.spec.transport !== "http") {
63
+ return null;
64
+ }
65
+ return upstream;
66
+ }
67
+ function getScopedHttpUpstream(config, upstreamFilter) {
68
+ if (upstreamFilter) {
69
+ const selected = config.servers[upstreamFilter];
70
+ if (!selected || selected.transport !== "http") {
71
+ return null;
72
+ }
73
+ return {
74
+ name: upstreamFilter,
75
+ spec: selected
76
+ };
77
+ }
78
+ return getSingleHttpUpstream(config);
79
+ }
80
+ function getWellKnownPrefix(pathname) {
81
+ for (const prefix of OAUTH_WELL_KNOWN_PREFIXES) {
82
+ if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
83
+ return prefix;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ function getUpstreamPathSuffixForWellKnown(upstream) {
89
+ const upstreamUrl = new URL(upstream.spec.url);
90
+ if (upstreamUrl.pathname === "/") {
91
+ return "";
92
+ }
93
+ return upstreamUrl.pathname.endsWith("/")
94
+ ? upstreamUrl.pathname.slice(0, -1)
95
+ : upstreamUrl.pathname;
96
+ }
97
+ function buildWellKnownUpstreamUrl(upstream, prefix) {
98
+ const upstreamUrl = new URL(upstream.spec.url);
99
+ const suffix = getUpstreamPathSuffixForWellKnown(upstream);
100
+ return new URL(`${prefix}${suffix}`, upstreamUrl.origin);
101
+ }
102
+ function getLocalOriginFromRequest(request) {
103
+ return `http://${request.headers.host ?? "127.0.0.1"}`;
104
+ }
105
+ function getRequestedUpstream(requestUrl) {
106
+ const value = requestUrl.searchParams.get("upstream");
107
+ if (!value) {
108
+ return undefined;
109
+ }
110
+ return value.trim() || undefined;
111
+ }
112
+ function appendUpstreamQuery(url, upstream) {
113
+ if (!upstream) {
114
+ return url;
115
+ }
116
+ const parsed = new URL(url);
117
+ parsed.searchParams.set("upstream", upstream);
118
+ return parsed.toString();
119
+ }
120
+ function rewriteWwwAuthenticateResourceMetadata(headerValue, localResourceMetadataUrl) {
121
+ if (headerValue.includes("resource_metadata=")) {
122
+ return headerValue.replace(/resource_metadata="[^"]*"/, `resource_metadata="${localResourceMetadataUrl}"`);
123
+ }
124
+ return `${headerValue}, resource_metadata="${localResourceMetadataUrl}"`;
125
+ }
126
+ function splitNamespacedName(value) {
127
+ const split = value.indexOf(".");
128
+ if (split <= 0 || split >= value.length - 1) {
129
+ return null;
130
+ }
131
+ return {
132
+ serverName: value.slice(0, split),
133
+ upstreamName: value.slice(split + 1)
134
+ };
135
+ }
136
+ function parseNamespacedUri(uri) {
137
+ if (uri.startsWith("mcpx://")) {
138
+ const rest = uri.slice("mcpx://".length);
139
+ const slashIndex = rest.indexOf("/");
140
+ if (slashIndex <= 0 || slashIndex >= rest.length - 1) {
141
+ return null;
142
+ }
143
+ const serverName = rest.slice(0, slashIndex);
144
+ const encoded = rest.slice(slashIndex + 1);
145
+ try {
146
+ return {
147
+ serverName,
148
+ upstreamUri: decodeURIComponent(encoded)
149
+ };
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ const nameSplit = splitNamespacedName(uri);
156
+ if (!nameSplit) {
157
+ return null;
158
+ }
159
+ return {
160
+ serverName: nameSplit.serverName,
161
+ upstreamUri: nameSplit.upstreamName
162
+ };
163
+ }
164
+ async function callUpstream(upstream, method, params, id, secrets, runtime, passthroughAuthorizationHeader) {
165
+ if (upstream.spec.transport === "stdio") {
166
+ return callStdioUpstream(upstream, method, params, secrets, runtime);
167
+ }
168
+ return callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader);
169
+ }
170
+ function getConfiguredTimeoutMs() {
171
+ const configuredTimeout = Number(process.env.MCPX_UPSTREAM_TIMEOUT_MS ?? DEFAULT_UPSTREAM_TIMEOUT_MS);
172
+ return Number.isFinite(configuredTimeout) && configuredTimeout > 0
173
+ ? configuredTimeout
174
+ : DEFAULT_UPSTREAM_TIMEOUT_MS;
175
+ }
176
+ function withTimeout(work, timeoutMs, timeoutMessage) {
177
+ return new Promise((resolve, reject) => {
178
+ const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
179
+ void work.then((value) => {
180
+ clearTimeout(timer);
181
+ resolve(value);
182
+ }, (error) => {
183
+ clearTimeout(timer);
184
+ reject(error);
185
+ });
186
+ });
187
+ }
188
+ function specFingerprint(spec) {
189
+ return JSON.stringify(spec);
190
+ }
191
+ function resolveStdioEnv(spec, secrets) {
192
+ if (!spec.env || Object.keys(spec.env).length === 0) {
193
+ return undefined;
194
+ }
195
+ const env = getDefaultEnvironment();
196
+ for (const [key, value] of Object.entries(spec.env)) {
197
+ env[key] = secrets.resolveMaybeSecret(value);
198
+ }
199
+ return env;
200
+ }
201
+ function buildStdioServerParameters(spec, secrets) {
202
+ return {
203
+ command: spec.command,
204
+ args: spec.args ?? [],
205
+ cwd: spec.cwd,
206
+ env: resolveStdioEnv(spec, secrets)
207
+ };
208
+ }
209
+ async function closeStdioConnection(entry) {
210
+ try {
211
+ const connection = await entry.promise;
212
+ await connection.transport.close();
213
+ }
214
+ catch {
215
+ // Ignore shutdown errors.
216
+ }
217
+ }
218
+ async function reconcileStdioConnections(config, runtime) {
219
+ const activeSpecs = new Map(Object.entries(config.servers).map(([name, spec]) => [name, specFingerprint(spec)]));
220
+ const staleNames = [];
221
+ for (const [name, entry] of runtime.stdioConnections.entries()) {
222
+ const expectedFingerprint = activeSpecs.get(name);
223
+ if (!expectedFingerprint || expectedFingerprint !== entry.fingerprint) {
224
+ staleNames.push(name);
225
+ }
226
+ }
227
+ for (const name of staleNames) {
228
+ const entry = runtime.stdioConnections.get(name);
229
+ if (!entry) {
230
+ continue;
231
+ }
232
+ runtime.stdioConnections.delete(name);
233
+ await closeStdioConnection(entry);
234
+ }
235
+ }
236
+ function invalidateStdioConnection(name, runtime) {
237
+ const existing = runtime.stdioConnections.get(name);
238
+ if (!existing) {
239
+ return;
240
+ }
241
+ runtime.stdioConnections.delete(name);
242
+ void closeStdioConnection(existing);
243
+ }
244
+ async function getStdioConnection(upstream, secrets, runtime) {
245
+ const fingerprint = specFingerprint(upstream.spec);
246
+ const existing = runtime.stdioConnections.get(upstream.name);
247
+ if (existing) {
248
+ if (existing.fingerprint === fingerprint) {
249
+ return existing.promise;
250
+ }
251
+ runtime.stdioConnections.delete(upstream.name);
252
+ void closeStdioConnection(existing);
253
+ }
254
+ const promise = (async () => {
255
+ const transport = new StdioClientTransport(buildStdioServerParameters(upstream.spec, secrets));
256
+ const client = new Client({
257
+ name: "mcpx",
258
+ version: SERVER_VERSION
259
+ });
260
+ await client.connect(transport);
261
+ return {
262
+ fingerprint,
263
+ client,
264
+ transport
265
+ };
266
+ })();
267
+ runtime.stdioConnections.set(upstream.name, {
268
+ fingerprint,
269
+ promise
270
+ });
271
+ try {
272
+ return await promise;
273
+ }
274
+ catch (error) {
275
+ runtime.stdioConnections.delete(upstream.name);
276
+ throw error;
277
+ }
278
+ }
279
+ async function callStdioUpstream(upstream, method, params, secrets, runtime) {
280
+ try {
281
+ const connection = await getStdioConnection(upstream, secrets, runtime);
282
+ const timeoutMs = getConfiguredTimeoutMs();
283
+ const timeoutMessage = `Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`;
284
+ if (method === "tools/list") {
285
+ return withTimeout(connection.client.listTools(params), timeoutMs, timeoutMessage);
286
+ }
287
+ if (method === "resources/list") {
288
+ return withTimeout(connection.client.listResources(params), timeoutMs, timeoutMessage);
289
+ }
290
+ if (method === "prompts/list") {
291
+ return withTimeout(connection.client.listPrompts(params), timeoutMs, timeoutMessage);
292
+ }
293
+ if (method === "tools/call") {
294
+ return withTimeout(connection.client.callTool(params), timeoutMs, timeoutMessage);
295
+ }
296
+ if (method === "resources/read") {
297
+ return withTimeout(connection.client.readResource(params), timeoutMs, timeoutMessage);
298
+ }
299
+ if (method === "prompts/get") {
300
+ return withTimeout(connection.client.getPrompt(params), timeoutMs, timeoutMessage);
301
+ }
302
+ throw new Error(`Unsupported stdio passthrough method: ${method}`);
303
+ }
304
+ catch (error) {
305
+ invalidateStdioConnection(upstream.name, runtime);
306
+ throw error;
307
+ }
308
+ }
309
+ async function callHttpUpstream(upstream, method, params, id, secrets, passthroughAuthorizationHeader) {
310
+ const headers = {
311
+ "content-type": "application/json",
312
+ accept: "application/json, text/event-stream"
313
+ };
314
+ for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
315
+ headers[key] = secrets.resolveMaybeSecret(value);
316
+ }
317
+ if (passthroughAuthorizationHeader) {
318
+ headers.Authorization = passthroughAuthorizationHeader;
319
+ }
320
+ const timeoutMs = getConfiguredTimeoutMs();
321
+ const timeoutController = new AbortController();
322
+ const timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
323
+ let response;
324
+ try {
325
+ response = await fetch(upstream.spec.url, {
326
+ method: "POST",
327
+ headers,
328
+ body: JSON.stringify({
329
+ jsonrpc: JSON_RPC_VERSION,
330
+ id,
331
+ method,
332
+ params
333
+ }),
334
+ signal: timeoutController.signal
335
+ });
336
+ }
337
+ catch (error) {
338
+ const name = error.name;
339
+ if (name === "AbortError") {
340
+ throw new Error(`Upstream ${upstream.name} timed out after ${timeoutMs}ms for method ${method}.`);
341
+ }
342
+ throw error;
343
+ }
344
+ finally {
345
+ clearTimeout(timeoutHandle);
346
+ }
347
+ if (!response.ok) {
348
+ const responseText = await response.text();
349
+ throw new UpstreamHttpError(upstream.name, response.status, responseText, response.headers.get("www-authenticate") ?? undefined);
350
+ }
351
+ let payload = null;
352
+ const contentType = response.headers.get("content-type") ?? "";
353
+ if (contentType.includes("application/json")) {
354
+ payload = JSON.parse(await response.text());
355
+ }
356
+ else if (contentType.includes("text/event-stream")) {
357
+ payload = await readSseJsonRpcResponse(response, id);
358
+ }
359
+ else {
360
+ try {
361
+ payload = JSON.parse(await response.text());
362
+ }
363
+ catch {
364
+ payload = null;
365
+ }
366
+ }
367
+ if (!payload) {
368
+ throw new Error(`Upstream ${upstream.name} response could not be parsed as JSON-RPC payload.`);
369
+ }
370
+ if (payload.error) {
371
+ throw new Error(`Upstream ${upstream.name} error: ${payload.error.message}`);
372
+ }
373
+ return payload.result;
374
+ }
375
+ async function readSseJsonRpcResponse(response, expectedId) {
376
+ if (!response.body) {
377
+ return null;
378
+ }
379
+ const decoder = new TextDecoder();
380
+ const reader = response.body.getReader();
381
+ let buffer = "";
382
+ let dataLines = [];
383
+ let latestPayload = null;
384
+ const consumeEvent = () => {
385
+ if (dataLines.length === 0) {
386
+ return null;
387
+ }
388
+ const combined = dataLines.join("\n").trim();
389
+ dataLines = [];
390
+ if (!combined || combined === "[DONE]") {
391
+ return null;
392
+ }
393
+ try {
394
+ const parsed = JSON.parse(combined);
395
+ latestPayload = parsed;
396
+ const parsedId = parsed.id ?? null;
397
+ if (parsedId === expectedId) {
398
+ return parsed;
399
+ }
400
+ return null;
401
+ }
402
+ catch {
403
+ return null;
404
+ }
405
+ };
406
+ try {
407
+ while (true) {
408
+ const { value, done } = await reader.read();
409
+ if (done) {
410
+ break;
411
+ }
412
+ buffer += decoder.decode(value, { stream: true });
413
+ while (true) {
414
+ const newlineIndex = buffer.indexOf("\n");
415
+ if (newlineIndex < 0) {
416
+ break;
417
+ }
418
+ let line = buffer.slice(0, newlineIndex);
419
+ buffer = buffer.slice(newlineIndex + 1);
420
+ if (line.endsWith("\r")) {
421
+ line = line.slice(0, -1);
422
+ }
423
+ if (line.startsWith("data:")) {
424
+ dataLines.push(line.slice("data:".length).trimStart());
425
+ continue;
426
+ }
427
+ if (line === "") {
428
+ const matched = consumeEvent();
429
+ if (matched) {
430
+ return matched;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ buffer += decoder.decode();
436
+ if (buffer.length > 0) {
437
+ let finalLine = buffer;
438
+ if (finalLine.endsWith("\r")) {
439
+ finalLine = finalLine.slice(0, -1);
440
+ }
441
+ if (finalLine.startsWith("data:")) {
442
+ dataLines.push(finalLine.slice("data:".length).trimStart());
443
+ }
444
+ }
445
+ return consumeEvent() ?? latestPayload;
446
+ }
447
+ finally {
448
+ try {
449
+ await reader.cancel();
450
+ }
451
+ catch {
452
+ // Ignore reader cancellation issues.
453
+ }
454
+ }
455
+ }
456
+ function authHeaderIsValid(request, expectedToken) {
457
+ const localTokenHeader = request.headers["x-mcpx-local-token"];
458
+ if (typeof localTokenHeader === "string" && localTokenHeader === expectedToken) {
459
+ return true;
460
+ }
461
+ if (Array.isArray(localTokenHeader) && localTokenHeader.includes(expectedToken)) {
462
+ return true;
463
+ }
464
+ const authHeader = request.headers.authorization;
465
+ if (!authHeader) {
466
+ return false;
467
+ }
468
+ const [scheme, token] = authHeader.split(" ");
469
+ return scheme === "Bearer" && token === expectedToken;
470
+ }
471
+ function isAuthChallenge(error) {
472
+ return error instanceof UpstreamHttpError && (error.status === 401 || error.status === 403);
473
+ }
474
+ function getClientAuthorizationForUpstream(request, expectedToken) {
475
+ const authHeader = request.headers.authorization;
476
+ if (!authHeader) {
477
+ return undefined;
478
+ }
479
+ const [scheme, token] = authHeader.split(" ");
480
+ if (scheme === "Bearer" && token === expectedToken) {
481
+ // Legacy local auth mode: Authorization is the local token, not an upstream OAuth token.
482
+ return undefined;
483
+ }
484
+ return authHeader;
485
+ }
486
+ async function handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
487
+ const tools = [];
488
+ const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
489
+ const flattenNames = Boolean(flattenedUpstream);
490
+ const upstreams = listUpstreams(config, upstreamFilter);
491
+ for (const upstream of upstreams) {
492
+ try {
493
+ const result = (await callUpstream(upstream, "tools/list", {}, `list-tools-${upstream.name}`, secrets, runtime, clientAuthorizationHeader));
494
+ for (const tool of result.tools ?? []) {
495
+ const name = typeof tool.name === "string" ? tool.name : "tool";
496
+ tools.push({
497
+ ...tool,
498
+ name: flattenNames ? name : `${upstream.name}.${name}`
499
+ });
500
+ }
501
+ }
502
+ catch (error) {
503
+ if (upstreams.length === 1 && isAuthChallenge(error)) {
504
+ throw error;
505
+ }
506
+ // Upstream errors are isolated so one failed server does not break catalog.
507
+ }
508
+ }
509
+ return { tools };
510
+ }
511
+ async function handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
512
+ const resources = [];
513
+ const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
514
+ const flattenNames = Boolean(flattenedUpstream);
515
+ const upstreams = listUpstreams(config, upstreamFilter);
516
+ for (const upstream of upstreams) {
517
+ try {
518
+ const result = (await callUpstream(upstream, "resources/list", {}, `list-resources-${upstream.name}`, secrets, runtime, clientAuthorizationHeader));
519
+ for (const resource of result.resources ?? []) {
520
+ const originalUri = typeof resource.uri === "string" ? resource.uri : "";
521
+ const originalName = typeof resource.name === "string" ? resource.name : originalUri;
522
+ resources.push({
523
+ ...resource,
524
+ name: flattenNames ? originalName : `${upstream.name}.${originalName}`,
525
+ uri: flattenNames ? originalUri : `mcpx://${upstream.name}/${encodeURIComponent(originalUri)}`
526
+ });
527
+ }
528
+ }
529
+ catch (error) {
530
+ if (upstreams.length === 1 && isAuthChallenge(error)) {
531
+ throw error;
532
+ }
533
+ // Upstream errors are isolated so one failed server does not break catalog.
534
+ }
535
+ }
536
+ return { resources };
537
+ }
538
+ async function handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
539
+ const prompts = [];
540
+ const flattenedUpstream = listUpstreams(config, upstreamFilter).length === 1;
541
+ const flattenNames = Boolean(flattenedUpstream);
542
+ const upstreams = listUpstreams(config, upstreamFilter);
543
+ for (const upstream of upstreams) {
544
+ try {
545
+ const result = (await callUpstream(upstream, "prompts/list", {}, `list-prompts-${upstream.name}`, secrets, runtime, clientAuthorizationHeader));
546
+ for (const prompt of result.prompts ?? []) {
547
+ const name = typeof prompt.name === "string" ? prompt.name : "prompt";
548
+ prompts.push({
549
+ ...prompt,
550
+ name: flattenNames ? name : `${upstream.name}.${name}`
551
+ });
552
+ }
553
+ }
554
+ catch (error) {
555
+ if (upstreams.length === 1 && isAuthChallenge(error)) {
556
+ throw error;
557
+ }
558
+ // Upstream errors are isolated so one failed server does not break catalog.
559
+ }
560
+ }
561
+ return { prompts };
562
+ }
563
+ async function routeNamespacedCall(config, method, params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
564
+ if (!params || typeof params !== "object") {
565
+ return makeError(id, -32602, "Missing params object.");
566
+ }
567
+ const upstreamEntries = listUpstreams(config, upstreamFilter);
568
+ const upstreams = new Map(upstreamEntries.map((entry) => [entry.name, entry]));
569
+ const flattenedUpstream = upstreamEntries.length === 1 ? upstreamEntries[0] : null;
570
+ if (method === "tools/call") {
571
+ const toolName = typeof params.name === "string" ? params.name : "";
572
+ const split = splitNamespacedName(toolName);
573
+ if (split && upstreamFilter && split.serverName !== upstreamFilter) {
574
+ return makeError(id, -32602, `Tool belongs to upstream ${split.serverName}, but request is scoped to ${upstreamFilter}.`);
575
+ }
576
+ let upstream;
577
+ let upstreamToolName = toolName;
578
+ if (split && upstreams.has(split.serverName)) {
579
+ upstream = upstreams.get(split.serverName);
580
+ upstreamToolName = split.upstreamName;
581
+ }
582
+ else if (flattenedUpstream) {
583
+ upstream = flattenedUpstream;
584
+ }
585
+ if (!upstream) {
586
+ return makeError(id, -32602, "Tool name must be namespaced as <server>.<tool>.");
587
+ }
588
+ const upstreamParams = {
589
+ ...params,
590
+ name: upstreamToolName
591
+ };
592
+ try {
593
+ const result = await callUpstream(upstream, method, upstreamParams, id, secrets, runtime, clientAuthorizationHeader);
594
+ return makeResult(id, result);
595
+ }
596
+ catch (error) {
597
+ if (isAuthChallenge(error)) {
598
+ throw error;
599
+ }
600
+ return makeError(id, -32000, error.message);
601
+ }
602
+ }
603
+ if (method === "prompts/get") {
604
+ const promptName = typeof params.name === "string" ? params.name : "";
605
+ const split = splitNamespacedName(promptName);
606
+ if (split && upstreamFilter && split.serverName !== upstreamFilter) {
607
+ return makeError(id, -32602, `Prompt belongs to upstream ${split.serverName}, but request is scoped to ${upstreamFilter}.`);
608
+ }
609
+ let upstream;
610
+ let upstreamPromptName = promptName;
611
+ if (split && upstreams.has(split.serverName)) {
612
+ upstream = upstreams.get(split.serverName);
613
+ upstreamPromptName = split.upstreamName;
614
+ }
615
+ else if (flattenedUpstream) {
616
+ upstream = flattenedUpstream;
617
+ }
618
+ if (!upstream) {
619
+ return makeError(id, -32602, "Prompt name must be namespaced as <server>.<prompt>.");
620
+ }
621
+ const upstreamParams = {
622
+ ...params,
623
+ name: upstreamPromptName
624
+ };
625
+ try {
626
+ const result = await callUpstream(upstream, method, upstreamParams, id, secrets, runtime, clientAuthorizationHeader);
627
+ return makeResult(id, result);
628
+ }
629
+ catch (error) {
630
+ if (isAuthChallenge(error)) {
631
+ throw error;
632
+ }
633
+ return makeError(id, -32000, error.message);
634
+ }
635
+ }
636
+ const uri = typeof params.uri === "string" ? params.uri : "";
637
+ const parsed = parseNamespacedUri(uri);
638
+ if (parsed && upstreamFilter && parsed.serverName !== upstreamFilter) {
639
+ return makeError(id, -32602, `Resource belongs to upstream ${parsed.serverName}, but request is scoped to ${upstreamFilter}.`);
640
+ }
641
+ let upstream;
642
+ let upstreamUri = uri;
643
+ if (parsed && upstreams.has(parsed.serverName)) {
644
+ upstream = upstreams.get(parsed.serverName);
645
+ upstreamUri = parsed.upstreamUri;
646
+ }
647
+ else if (flattenedUpstream) {
648
+ upstream = flattenedUpstream;
649
+ }
650
+ if (!upstream) {
651
+ return makeError(id, -32602, "Resource URI must be namespaced (mcpx://<server>/<encoded-uri>).", { uri });
652
+ }
653
+ const upstreamParams = {
654
+ ...params,
655
+ uri: upstreamUri
656
+ };
657
+ try {
658
+ const result = await callUpstream(upstream, method, upstreamParams, id, secrets, runtime, clientAuthorizationHeader);
659
+ return makeResult(id, result);
660
+ }
661
+ catch (error) {
662
+ if (isAuthChallenge(error)) {
663
+ throw error;
664
+ }
665
+ return makeError(id, -32000, error.message);
666
+ }
667
+ }
668
+ async function handleRequestObject(request, secrets, runtime, upstreamFilter, clientAuthorizationHeader) {
669
+ const id = request.id ?? null;
670
+ if (!request.method || typeof request.method !== "string") {
671
+ return makeError(id, -32600, "Invalid JSON-RPC request: missing method.");
672
+ }
673
+ if (request.method === "initialize") {
674
+ const requestedProtocol = request.params?.protocolVersion;
675
+ const protocolVersion = typeof requestedProtocol === "string" && requestedProtocol.length > 0
676
+ ? requestedProtocol
677
+ : "2025-11-25";
678
+ return makeResult(id, {
679
+ protocolVersion,
680
+ capabilities: {
681
+ tools: {},
682
+ resources: {},
683
+ prompts: {}
684
+ },
685
+ serverInfo: {
686
+ name: "mcpx",
687
+ version: SERVER_VERSION
688
+ }
689
+ });
690
+ }
691
+ if (request.method === "notifications/initialized") {
692
+ return null;
693
+ }
694
+ if (request.method === "ping") {
695
+ return makeResult(id, { ok: true });
696
+ }
697
+ const config = loadConfig();
698
+ if (upstreamFilter && !config.servers[upstreamFilter]) {
699
+ return makeError(id, -32602, `Unknown upstream: ${upstreamFilter}`);
700
+ }
701
+ await reconcileStdioConnections(config, runtime);
702
+ if (request.method === "tools/list") {
703
+ const result = await handleListTools(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
704
+ return makeResult(id, result);
705
+ }
706
+ if (request.method === "resources/list") {
707
+ const result = await handleListResources(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
708
+ return makeResult(id, result);
709
+ }
710
+ if (request.method === "prompts/list") {
711
+ const result = await handleListPrompts(config, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
712
+ return makeResult(id, result);
713
+ }
714
+ if (request.method === "tools/call" || request.method === "resources/read" || request.method === "prompts/get") {
715
+ return routeNamespacedCall(config, request.method, request.params, id, secrets, runtime, upstreamFilter, clientAuthorizationHeader);
716
+ }
717
+ return makeError(id, -32601, `Unsupported method: ${request.method}`);
718
+ }
719
+ async function maybeHandleWellKnownOAuthRequest(request, response, requestUrl, secrets) {
720
+ const pathname = requestUrl.pathname;
721
+ const wellKnownPrefix = getWellKnownPrefix(pathname);
722
+ if (!wellKnownPrefix) {
723
+ return false;
724
+ }
725
+ if (request.method !== "GET") {
726
+ response.statusCode = 405;
727
+ response.setHeader("content-type", "application/json");
728
+ response.end(JSON.stringify({ error: "method_not_allowed" }));
729
+ return true;
730
+ }
731
+ const config = loadConfig();
732
+ const requestedUpstream = getRequestedUpstream(requestUrl);
733
+ const upstream = getScopedHttpUpstream(config, requestedUpstream);
734
+ if (!upstream) {
735
+ response.statusCode = 404;
736
+ response.setHeader("content-type", "application/json");
737
+ response.end(JSON.stringify({ error: "not_found" }));
738
+ return true;
739
+ }
740
+ const upstreamWellKnownUrl = buildWellKnownUpstreamUrl(upstream, wellKnownPrefix);
741
+ const headers = {
742
+ accept: "application/json"
743
+ };
744
+ const protocolVersion = request.headers["mcp-protocol-version"];
745
+ if (typeof protocolVersion === "string" && protocolVersion.length > 0) {
746
+ headers["mcp-protocol-version"] = protocolVersion;
747
+ }
748
+ for (const [key, value] of Object.entries(upstream.spec.headers ?? {})) {
749
+ headers[key] = secrets.resolveMaybeSecret(value);
750
+ }
751
+ const upstreamResponse = await fetch(upstreamWellKnownUrl, {
752
+ method: "GET",
753
+ headers
754
+ });
755
+ let bodyText = await upstreamResponse.text();
756
+ response.statusCode = upstreamResponse.status;
757
+ const contentType = upstreamResponse.headers.get("content-type");
758
+ if (contentType) {
759
+ response.setHeader("content-type", contentType);
760
+ }
761
+ const cacheControl = upstreamResponse.headers.get("cache-control");
762
+ if (cacheControl) {
763
+ response.setHeader("cache-control", cacheControl);
764
+ }
765
+ const wwwAuthenticate = upstreamResponse.headers.get("www-authenticate");
766
+ if (wwwAuthenticate) {
767
+ response.setHeader("www-authenticate", wwwAuthenticate);
768
+ }
769
+ if (wellKnownPrefix === "/.well-known/oauth-protected-resource"
770
+ && contentType?.includes("application/json")
771
+ && upstreamResponse.ok) {
772
+ try {
773
+ const parsed = JSON.parse(bodyText);
774
+ parsed.resource = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/mcp`, requestedUpstream);
775
+ bodyText = JSON.stringify(parsed);
776
+ }
777
+ catch {
778
+ // Keep original body if parsing fails.
779
+ }
780
+ }
781
+ response.end(bodyText);
782
+ return true;
783
+ }
784
+ export function createGatewayServer(options) {
785
+ const debug = process.env.MCPX_GATEWAY_DEBUG === "1";
786
+ const runtime = {
787
+ stdioConnections: new Map()
788
+ };
789
+ const server = http.createServer(async (request, response) => {
790
+ let requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
791
+ try {
792
+ requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
793
+ const upstreamFilter = getRequestedUpstream(requestUrl);
794
+ if (debug) {
795
+ console.error(`[mcpx gateway] ${request.method ?? "?"} ${requestUrl.pathname} auth=${request.headers.authorization ? "yes" : "no"} accept=${request.headers.accept ?? ""}`);
796
+ console.error(`[mcpx gateway] headers=${JSON.stringify(request.headers)}`);
797
+ }
798
+ if (await maybeHandleWellKnownOAuthRequest(request, response, requestUrl, options.secrets)) {
799
+ if (debug) {
800
+ console.error(`[mcpx gateway] -> ${response.statusCode} (well-known oauth)`);
801
+ }
802
+ return;
803
+ }
804
+ if (requestUrl.pathname !== "/mcp") {
805
+ response.statusCode = 404;
806
+ response.setHeader("content-type", "application/json");
807
+ response.end(JSON.stringify({ error: "not_found" }));
808
+ if (debug) {
809
+ console.error(`[mcpx gateway] -> 404`);
810
+ }
811
+ return;
812
+ }
813
+ if (request.method === "GET") {
814
+ if (!authHeaderIsValid(request, options.expectedToken)) {
815
+ response.statusCode = 401;
816
+ response.setHeader("content-type", "application/json");
817
+ response.end(JSON.stringify(makeError(null, -32001, "Unauthorized")));
818
+ if (debug) {
819
+ console.error(`[mcpx gateway] -> 401 (GET unauthorized)`);
820
+ }
821
+ return;
822
+ }
823
+ response.statusCode = 200;
824
+ response.setHeader("content-type", "application/json");
825
+ response.end(JSON.stringify({ ok: true, server: "mcpx" }));
826
+ if (debug) {
827
+ console.error(`[mcpx gateway] -> 200 (GET ok)`);
828
+ }
829
+ return;
830
+ }
831
+ if (request.method !== "POST") {
832
+ response.statusCode = 405;
833
+ response.end("Method Not Allowed");
834
+ if (debug) {
835
+ console.error(`[mcpx gateway] -> 405`);
836
+ }
837
+ return;
838
+ }
839
+ if (!authHeaderIsValid(request, options.expectedToken)) {
840
+ response.statusCode = 401;
841
+ response.setHeader("content-type", "application/json");
842
+ response.end(JSON.stringify(makeError(null, -32001, "Unauthorized")));
843
+ if (debug) {
844
+ console.error(`[mcpx gateway] -> 401 (POST unauthorized)`);
845
+ }
846
+ return;
847
+ }
848
+ let body = "";
849
+ request.setEncoding("utf8");
850
+ for await (const chunk of request) {
851
+ body += chunk;
852
+ if (body.length > 2_000_000) {
853
+ response.statusCode = 413;
854
+ response.end("Payload Too Large");
855
+ if (debug) {
856
+ console.error(`[mcpx gateway] -> 413`);
857
+ }
858
+ return;
859
+ }
860
+ }
861
+ const parsed = JSON.parse(body);
862
+ const hasInitialize = Array.isArray(parsed)
863
+ ? parsed.some((item) => item.method === "initialize")
864
+ : parsed.method === "initialize";
865
+ const responses = [];
866
+ const clientAuthorizationHeader = getClientAuthorizationForUpstream(request, options.expectedToken);
867
+ if (Array.isArray(parsed)) {
868
+ for (const item of parsed) {
869
+ if (debug) {
870
+ console.error(`[mcpx gateway] rpc method=${item.method} id=${item.id ?? "null"}`);
871
+ }
872
+ const rpcResponse = await handleRequestObject(item, options.secrets, runtime, upstreamFilter, clientAuthorizationHeader);
873
+ if (debug && rpcResponse?.error) {
874
+ console.error(`[mcpx gateway] rpc error code=${rpcResponse.error.code} message=${rpcResponse.error.message}`);
875
+ }
876
+ if (rpcResponse) {
877
+ responses.push(rpcResponse);
878
+ }
879
+ }
880
+ }
881
+ else {
882
+ if (debug) {
883
+ console.error(`[mcpx gateway] rpc method=${parsed.method} id=${parsed.id ?? "null"}`);
884
+ if (parsed.method === "initialize") {
885
+ console.error(`[mcpx gateway] rpc initialize params=${JSON.stringify(parsed.params ?? {})}`);
886
+ }
887
+ }
888
+ const rpcResponse = await handleRequestObject(parsed, options.secrets, runtime, upstreamFilter, clientAuthorizationHeader);
889
+ if (debug && rpcResponse?.error) {
890
+ console.error(`[mcpx gateway] rpc error code=${rpcResponse.error.code} message=${rpcResponse.error.message}`);
891
+ }
892
+ if (rpcResponse) {
893
+ responses.push(rpcResponse);
894
+ }
895
+ }
896
+ response.statusCode = 200;
897
+ const acceptsSse = (request.headers.accept ?? "").includes("text/event-stream");
898
+ if (acceptsSse) {
899
+ response.setHeader("content-type", "text/event-stream");
900
+ response.setHeader("cache-control", "no-cache");
901
+ response.setHeader("connection", "keep-alive");
902
+ }
903
+ else {
904
+ response.setHeader("content-type", "application/json");
905
+ }
906
+ if (hasInitialize) {
907
+ const sessionId = crypto.randomUUID();
908
+ response.setHeader("mcp-session-id", sessionId);
909
+ response.setHeader("MCP-Session-Id", sessionId);
910
+ }
911
+ if (debug) {
912
+ console.error(`[mcpx gateway] -> 200 (rpc)`);
913
+ }
914
+ if (acceptsSse) {
915
+ const payloads = responses.length > 0 ? responses : [];
916
+ for (const payload of payloads) {
917
+ response.write(`event: message\n`);
918
+ response.write(`data: ${JSON.stringify(payload)}\n\n`);
919
+ }
920
+ response.end();
921
+ }
922
+ else if (responses.length === 1) {
923
+ response.end(JSON.stringify(responses[0]));
924
+ }
925
+ else {
926
+ response.end(JSON.stringify(responses));
927
+ }
928
+ }
929
+ catch (error) {
930
+ if (isAuthChallenge(error)) {
931
+ response.statusCode = error.status;
932
+ response.setHeader("content-type", "application/json");
933
+ if (error.wwwAuthenticate) {
934
+ const localResourceMetadataUrl = appendUpstreamQuery(`${getLocalOriginFromRequest(request)}/.well-known/oauth-protected-resource`, getRequestedUpstream(requestUrl));
935
+ response.setHeader("www-authenticate", rewriteWwwAuthenticateResourceMetadata(error.wwwAuthenticate, localResourceMetadataUrl));
936
+ }
937
+ response.end(error.bodyText.length > 0 ? error.bodyText : JSON.stringify({ error: "upstream_auth_required" }));
938
+ if (debug) {
939
+ console.error(`[mcpx gateway] -> ${error.status} upstream auth challenge`);
940
+ }
941
+ return;
942
+ }
943
+ response.statusCode = 500;
944
+ response.setHeader("content-type", "application/json");
945
+ response.end(JSON.stringify(makeError(null, -32000, error.message)));
946
+ if (debug) {
947
+ console.error(`[mcpx gateway] -> 500 ${error.message}`);
948
+ }
949
+ }
950
+ });
951
+ server.on("close", () => {
952
+ for (const entry of runtime.stdioConnections.values()) {
953
+ void closeStdioConnection(entry);
954
+ }
955
+ runtime.stdioConnections.clear();
956
+ });
957
+ server.listen(options.port, "127.0.0.1");
958
+ return server;
959
+ }
960
+ //# sourceMappingURL=server.js.map