@teampitch/mcpx 0.2.1 → 0.3.1

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/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env bun
1
2
  import { readFileSync } from "node:fs";
2
3
  import { join, dirname } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
@@ -18,6 +19,14 @@ import {
18
19
  import { loadConfig } from "./config.js";
19
20
  import { executeCode } from "./executor.js";
20
21
  import { createOAuthRoutes } from "./oauth.js";
22
+ import {
23
+ loadSkills,
24
+ registerSkill,
25
+ searchSkills,
26
+ recordExecution,
27
+ watchSkills,
28
+ generateSkillTypeDefs,
29
+ } from "./skills.js";
21
30
  import { startStdioServer } from "./stdio.js";
22
31
  import { watchConfig } from "./watcher.js";
23
32
 
@@ -45,320 +54,383 @@ if (command === "stdio") {
45
54
  }
46
55
 
47
56
  // HTTP server mode (default)
48
- const configPath = process.argv[2] ?? "mcpx.json";
49
-
50
- let config;
51
- try {
52
- config = loadConfig(configPath);
53
- } catch (err) {
54
- const msg =
55
- (err as NodeJS.ErrnoException).code === "ENOENT"
56
- ? `Config file not found: ${configPath}\n Create it or pass the path as an argument: mcpx <config.json>`
57
- : `Failed to load config from ${configPath}: ${(err as Error).message}`;
58
- console.error(`mcpx startup error: ${msg}`);
59
- process.exit(1);
60
- }
57
+ if (command !== "stdio") {
58
+ const configPath = process.argv[2] ?? "mcpx.json";
59
+
60
+ let config;
61
+ try {
62
+ config = loadConfig(configPath);
63
+ } catch (err) {
64
+ const msg =
65
+ (err as NodeJS.ErrnoException).code === "ENOENT"
66
+ ? `Config file not found: ${configPath}\n Create it or pass the path as an argument: mcpx <config.json>`
67
+ : `Failed to load config from ${configPath}: ${(err as Error).message}`;
68
+ console.error(`mcpx startup error: ${msg}`);
69
+ process.exit(1);
70
+ }
61
71
 
62
- console.log("mcpx starting...");
63
- console.log(` version: ${VERSION}`);
64
- console.log(` config: ${configPath}`);
65
- console.log(` port: ${config.port}`);
66
- console.log(` backends: ${Object.keys(config.backends).join(", ")}`);
67
-
68
- // Connect to all backend MCP servers
69
- console.log("\nConnecting to backends:");
70
- let backends: Map<string, import("./backends.js").Backend>;
71
- try {
72
- backends = await connectBackends(config.backends);
73
- } catch (err) {
74
- console.error(`Failed to connect backends: ${(err as Error).message}`);
75
- process.exit(1);
76
- }
72
+ console.log("mcpx starting...");
73
+ console.log(` version: ${VERSION}`);
74
+ console.log(` config: ${configPath}`);
75
+ console.log(` port: ${config.port}`);
76
+ console.log(` backends: ${Object.keys(config.backends).join(", ")}`);
77
+
78
+ // Connect to all backend MCP servers
79
+ console.log("\nConnecting to backends:");
80
+ let backends: Map<string, import("./backends.js").Backend>;
81
+ try {
82
+ const tokensDir = join(configPath.replace(/[^/]+$/, ""), ".mcpx", "tokens");
83
+ backends = await connectBackends(config.backends, { tokensDir });
84
+ } catch (err) {
85
+ console.error(`Failed to connect backends: ${(err as Error).message}`);
86
+ process.exit(1);
87
+ }
77
88
 
78
- if (backends.size === 0 && !config.failOpen) {
79
- console.error(
80
- "No backends connected. Check that your backend commands are installed and accessible.\n Use failOpen: true in config to start anyway.",
81
- );
82
- process.exit(1);
83
- }
89
+ if (backends.size === 0 && !config.failOpen) {
90
+ console.error(
91
+ "No backends connected. Check that your backend commands are installed and accessible.\n Use failOpen: true in config to start anyway.",
92
+ );
93
+ process.exit(1);
94
+ }
84
95
 
85
- if (backends.size === 0) {
86
- console.warn("Warning: no backends connected (failOpen mode — server will start degraded)");
87
- }
96
+ if (backends.size === 0) {
97
+ console.warn("Warning: no backends connected (failOpen mode — server will start degraded)");
98
+ }
88
99
 
89
- // Pre-generate type definitions and tool listing (mutable for hot-reload + tool refresh)
90
- let typeDefs = generateTypeDefinitions(backends);
91
- let toolListing = generateToolListing(backends);
92
-
93
- let totalTools = Array.from(backends.values()).reduce((sum, b) => sum + b.tools.length, 0);
94
- console.log(`\n${totalTools} tools from ${backends.size} backends → 2 Code Mode tools`);
95
-
96
- // Periodic tool refresh
97
- if (config.toolRefreshInterval && config.toolRefreshInterval > 0) {
98
- setInterval(async () => {
99
- try {
100
- await refreshAllTools(backends);
101
- typeDefs = generateTypeDefinitions(backends);
102
- toolListing = generateToolListing(backends);
103
- totalTools = Array.from(backends.values()).reduce((sum, b) => sum + b.tools.length, 0);
104
- } catch (err) {
105
- console.error("Tool refresh failed:", (err as Error).message);
106
- }
107
- }, config.toolRefreshInterval * 1000);
108
- }
100
+ // Load skills from .mcpx/skills/
101
+ const skillsDir = join(configPath.replace(/[^/]+$/, ""), ".mcpx", "skills");
102
+ const skills = loadSkills(skillsDir);
103
+ if (skills.size > 0) console.log(` ${skills.size} skills loaded from ${skillsDir}`);
104
+ watchSkills(skillsDir, skills, () => {
105
+ console.log(`Skills reloaded (${skills.size} skills)`);
106
+ });
107
+
108
+ // Pre-generate type definitions and tool listing (mutable for hot-reload + tool refresh)
109
+ let typeDefs = generateTypeDefinitions(backends);
110
+ let skillTypeDefs = generateSkillTypeDefs(skills);
111
+ let toolListing = generateToolListing(backends);
112
+
113
+ let totalTools = Array.from(backends.values()).reduce((sum, b) => sum + b.tools.length, 0);
114
+ console.log(`\n${totalTools} tools from ${backends.size} backends 2 Code Mode tools`);
115
+
116
+ // Periodic tool refresh
117
+ if (config.toolRefreshInterval && config.toolRefreshInterval > 0) {
118
+ setInterval(async () => {
119
+ try {
120
+ await refreshAllTools(backends);
121
+ typeDefs = generateTypeDefinitions(backends);
122
+ toolListing = generateToolListing(backends);
123
+ totalTools = Array.from(backends.values()).reduce((sum, b) => sum + b.tools.length, 0);
124
+ } catch (err) {
125
+ console.error("Tool refresh failed:", (err as Error).message);
126
+ }
127
+ }, config.toolRefreshInterval * 1000);
128
+ }
109
129
 
110
- // Hot-reload: watch config file for changes
111
- watchConfig(configPath, backends, (newConfig, diff) => {
112
- config = newConfig;
113
- typeDefs = generateTypeDefinitions(backends);
114
- toolListing = generateToolListing(backends);
115
- totalTools = Array.from(backends.values()).reduce((sum, b) => sum + b.tools.length, 0);
116
- console.log(
117
- `Config reloaded: +${diff.added.length} -${diff.removed.length} ~${diff.changed.length} (${totalTools} tools)`,
118
- );
119
- });
120
-
121
- // Create the MCP server with 2 Code Mode tools
122
- function createMcpServer(visibleBackends: Map<string, Backend>): McpServer {
123
- const server = new McpServer({
124
- name: "mcpx",
125
- version: VERSION,
130
+ // Hot-reload: watch config file for changes
131
+ watchConfig(configPath, backends, (newConfig, diff) => {
132
+ config = newConfig;
133
+ typeDefs = generateTypeDefinitions(backends);
134
+ toolListing = generateToolListing(backends);
135
+ totalTools = Array.from(backends.values()).reduce((sum, b) => sum + b.tools.length, 0);
136
+ console.log(
137
+ `Config reloaded: +${diff.added.length} -${diff.removed.length} ~${diff.changed.length} (${totalTools} tools)`,
138
+ );
126
139
  });
127
140
 
128
- server.tool(
129
- "search",
130
- `Search available tools across all connected MCP servers. Returns type definitions for matched tools.
141
+ // Create the MCP server with Code Mode tools + skill management
142
+ function createMcpServer(visibleBackends: Map<string, Backend>): McpServer {
143
+ const server = new McpServer({
144
+ name: "mcpx",
145
+ version: VERSION,
146
+ });
147
+
148
+ server.tool(
149
+ "search",
150
+ `Search available tools and skills. Returns type definitions for matched tools.
131
151
 
132
152
  Available tools:
133
153
  ${toolListing}`,
134
- {
135
- query: z.string().describe("Search query — tool name, backend name, or keyword"),
136
- },
137
- async ({ query }) => {
138
- const q = query.toLowerCase();
139
- const matched: string[] = [];
140
-
141
- for (const [name, backend] of visibleBackends) {
142
- for (const tool of backend.tools) {
143
- const fullName = `${name}_${tool.name}`;
144
- const desc = tool.description?.toLowerCase() ?? "";
145
- if (
146
- fullName.toLowerCase().includes(q) ||
147
- desc.includes(q) ||
148
- name.toLowerCase().includes(q)
149
- ) {
150
- const params = tool.inputSchema?.properties
151
- ? JSON.stringify(tool.inputSchema.properties, null, 2)
152
- : "{}";
153
- matched.push(`### ${fullName}\n${tool.description ?? ""}\nParameters: ${params}`);
154
+ {
155
+ query: z
156
+ .string()
157
+ .describe("Search query tool name, backend name, skill name, or keyword"),
158
+ },
159
+ async ({ query }) => {
160
+ const q = query.toLowerCase();
161
+ const matched: string[] = [];
162
+
163
+ for (const [name, backend] of visibleBackends) {
164
+ for (const tool of backend.tools) {
165
+ const fullName = `${name}_${tool.name}`;
166
+ const desc = tool.description?.toLowerCase() ?? "";
167
+ if (
168
+ fullName.toLowerCase().includes(q) ||
169
+ desc.includes(q) ||
170
+ name.toLowerCase().includes(q)
171
+ ) {
172
+ const params = tool.inputSchema?.properties
173
+ ? JSON.stringify(tool.inputSchema.properties, null, 2)
174
+ : "{}";
175
+ matched.push(`### ${fullName}\n${tool.description ?? ""}\nParameters: ${params}`);
176
+ }
154
177
  }
155
178
  }
156
- }
157
179
 
158
- if (matched.length === 0) {
180
+ // Also search skills
181
+ const matchedSkills = searchSkills(skills, query);
182
+ for (const s of matchedSkills) {
183
+ matched.push(
184
+ `### skill.${s.name} [${s.trust}]\n${s.description}\nCode: ${s.code.slice(0, 200)}${s.code.length > 200 ? "..." : ""}`,
185
+ );
186
+ }
187
+
188
+ if (matched.length === 0) {
189
+ return {
190
+ content: [
191
+ {
192
+ type: "text" as const,
193
+ text: `No tools or skills found matching "${query}".`,
194
+ },
195
+ ],
196
+ };
197
+ }
198
+
159
199
  return {
160
200
  content: [
161
201
  {
162
202
  type: "text" as const,
163
- text: `No tools found matching "${query}". Available backends: ${Array.from(visibleBackends.keys()).join(", ")}`,
203
+ text: `Found ${matched.length} results:\n\n${matched.join("\n\n")}`,
164
204
  },
165
205
  ],
166
206
  };
167
- }
207
+ },
208
+ );
168
209
 
169
- return {
170
- content: [
171
- {
172
- type: "text" as const,
173
- text: `Found ${matched.length} tools:\n\n${matched.join("\n\n")}`,
174
- },
175
- ],
176
- };
177
- },
178
- );
179
-
180
- server.tool(
181
- "execute",
182
- `Execute JavaScript code that calls MCP tools. The code runs in a V8 isolate.
210
+ server.tool(
211
+ "execute",
212
+ `Execute JavaScript code that calls MCP tools. The code runs in a V8 isolate.
183
213
 
184
214
  Write an async function body. Available tool functions (call with await):
185
215
  ${typeDefs}
216
+ ${skillTypeDefs}
186
217
 
187
- Example (namespace style):
218
+ Example:
188
219
  const result = await grafana.searchDashboards({ query: "pods" });
189
- return result;
190
-
191
- Example (classic style):
192
- const result = await grafana_search_dashboards({ query: "pods" });
193
220
  return result;`,
194
- { code: z.string().describe("JavaScript async function body to execute") },
195
- async ({ code }) => {
196
- const result = await executeCode(code, visibleBackends);
197
-
198
- if (result.isErr()) {
199
- const e = result.error;
200
- let msg = e.kind === "runtime" ? `Execution failed with code ${e.code}` : e.message;
201
- if (e.kind === "parse" && e.snippet) {
202
- msg += `\n\n${e.snippet}`;
221
+ {
222
+ code: z.string().describe("JavaScript async function body to execute"),
223
+ },
224
+ async ({ code }) => {
225
+ const result = await executeCode(code, visibleBackends, { skills });
226
+
227
+ if (result.isErr()) {
228
+ const e = result.error;
229
+ let msg = e.kind === "runtime" ? `Execution failed with code ${e.code}` : e.message;
230
+ if (e.kind === "parse" && e.snippet) msg += `\n\n${e.snippet}`;
231
+ return {
232
+ content: [{ type: "text" as const, text: `Error: ${msg}` }],
233
+ isError: true,
234
+ };
203
235
  }
236
+
237
+ const val = result.value.value;
238
+ const text = typeof val === "string" ? val : JSON.stringify(val, null, 2);
239
+ const logText =
240
+ result.value.logs.length > 0
241
+ ? `\n\n--- Console Output ---\n${result.value.logs.map((l) => `[${l.level}] ${l.args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")}`).join("\n")}`
242
+ : "";
243
+ const eventText =
244
+ result.value.events.filter((e) => e.type === "tool_call" || e.type === "tool_error")
245
+ .length > 0
246
+ ? `\n\n--- Execution Events ---\n${result.value.events
247
+ .filter((e) => e.type !== "console")
248
+ .map(
249
+ (e) =>
250
+ `[${e.type}] ${e.tool ?? ""}${e.durationMs ? ` (${e.durationMs}ms)` : ""}${e.error ? ` ERROR: ${e.error}` : ""}`,
251
+ )
252
+ .join("\n")}`
253
+ : "";
254
+
204
255
  return {
205
- content: [{ type: "text" as const, text: `Error: ${msg}` }],
206
- isError: true,
256
+ content: [{ type: "text" as const, text: text + logText + eventText }],
207
257
  };
208
- }
258
+ },
259
+ );
260
+
261
+ server.tool(
262
+ "register_skill",
263
+ "Save working code as a reusable skill. The skill becomes available to all agents connected to this gateway.",
264
+ {
265
+ name: z.string().describe("Skill name (alphanumeric + hyphens)"),
266
+ description: z.string().describe("What this skill does"),
267
+ code: z.string().describe("JavaScript async function body (same as execute code)"),
268
+ },
269
+ async ({ name, description, code: skillCode }) => {
270
+ const skill = registerSkill(skillsDir, skills, {
271
+ name,
272
+ description,
273
+ code: skillCode,
274
+ });
275
+ skillTypeDefs = generateSkillTypeDefs(skills);
276
+ return {
277
+ content: [
278
+ {
279
+ type: "text" as const,
280
+ text: `Skill "${skill.name}" registered (${skill.trust}). Available as skill.${skill.name}() in execute.`,
281
+ },
282
+ ],
283
+ };
284
+ },
285
+ );
209
286
 
210
- const val = result.value.value;
211
- const text = typeof val === "string" ? val : JSON.stringify(val, null, 2);
212
- const logText =
213
- result.value.logs.length > 0
214
- ? `\n\n--- Console Output ---\n${result.value.logs.map((l) => `[${l.level}] ${l.args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")}`).join("\n")}`
215
- : "";
216
-
217
- return {
218
- content: [
219
- {
220
- type: "text" as const,
221
- text: text + logText,
222
- },
223
- ],
224
- };
225
- },
226
- );
227
-
228
- return server;
229
- }
287
+ return server;
288
+ }
230
289
 
231
- // HTTP server with Hono
232
- const app = new Hono();
233
-
234
- // Record start time for uptime reporting
235
- const startedAt = Date.now();
236
-
237
- // Health check — includes uptime, version, and per-backend tool counts
238
- app.get("/health", (c) => {
239
- const backendDetails = Array.from(backends.entries()).map(([name, backend]) => ({
240
- name,
241
- tools: backend.tools.length,
242
- }));
243
-
244
- return c.json({
245
- status: backends.size === 0 ? "degraded" : "ok",
246
- version: VERSION,
247
- uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000),
248
- backends: backendDetails,
249
- totalTools,
290
+ // HTTP server with Hono
291
+ const app = new Hono();
292
+
293
+ // Record start time for uptime reporting
294
+ const startedAt = Date.now();
295
+
296
+ // Health check — includes uptime, version, and per-backend tool counts
297
+ app.get("/health", (c) => {
298
+ const backendDetails = Array.from(backends.entries()).map(([name, backend]) => ({
299
+ name,
300
+ tools: backend.tools.length,
301
+ }));
302
+
303
+ return c.json({
304
+ status: backends.size === 0 ? "degraded" : "ok",
305
+ version: VERSION,
306
+ uptimeSeconds: Math.floor((Date.now() - startedAt) / 1000),
307
+ backends: backendDetails,
308
+ totalTools,
309
+ });
250
310
  });
251
- });
252
311
 
253
- // Mount OAuth routes if configured
254
- if (config.auth?.oauth) {
255
- createOAuthRoutes(config.auth.oauth, app);
256
- }
312
+ // Mount OAuth routes if configured
313
+ if (config.auth?.oauth) {
314
+ createOAuthRoutes(config.auth.oauth, app);
315
+ }
257
316
 
258
- // Auth middleware — JWT, bearer, OAuth, or open
259
- const verifier = createAuthVerifier(config);
260
- if (verifier) {
261
- app.use("/mcp", async (c, next) => {
262
- const authHeader = c.req.header("Authorization");
263
- const token = authHeader?.replace(/^Bearer\s+/i, "");
264
- if (!token) {
265
- // Per MCP OAuth spec — include metadata URL in 401 response
266
- const headers: Record<string, string> = {};
267
- if (config.auth?.oauth) {
268
- headers["WWW-Authenticate"] =
269
- `Bearer resource_metadata="/.well-known/oauth-authorization-server"`;
317
+ // Auth middleware — JWT, bearer, OAuth, or open
318
+ const verifier = createAuthVerifier(config);
319
+ if (verifier) {
320
+ app.use("/mcp", async (c, next) => {
321
+ const authHeader = c.req.header("Authorization");
322
+ const token = authHeader?.replace(/^Bearer\s+/i, "");
323
+ if (!token) {
324
+ // Per MCP OAuth spec — include metadata URL in 401 response
325
+ const headers: Record<string, string> = {};
326
+ if (config.auth?.oauth) {
327
+ headers["WWW-Authenticate"] =
328
+ `Bearer resource_metadata="/.well-known/oauth-authorization-server"`;
329
+ }
330
+ return c.json({ error: "unauthorized" }, { status: 401, headers });
270
331
  }
271
- return c.json({ error: "unauthorized" }, { status: 401, headers });
332
+
333
+ const result = await verifier(token);
334
+ if (result.isErr()) return c.json({ error: result.error }, 401);
335
+
336
+ // Store claims for per-backend filtering
337
+ c.set("claims" as never, result.value as never);
338
+ await next();
339
+ });
340
+ }
341
+
342
+ // Session management for stateful MCP connections
343
+ const sessions = new Map<
344
+ string,
345
+ {
346
+ server: McpServer;
347
+ transport: WebStandardStreamableHTTPServerTransport;
348
+ lastAccess: number;
349
+ }
350
+ >();
351
+ const sessionTtlMs = (config.sessionTtlMinutes ?? 30) * 60 * 1000;
352
+
353
+ // Expire stale sessions every minute
354
+ setInterval(() => {
355
+ const now = Date.now();
356
+ for (const [id, session] of sessions) {
357
+ if (now - session.lastAccess > sessionTtlMs) {
358
+ sessions.delete(id);
359
+ }
360
+ }
361
+ }, 60_000);
362
+
363
+ // MCP endpoint — Streamable HTTP with session support
364
+ app.all("/mcp", async (c) => {
365
+ // Resolve visible backends based on auth claims
366
+ const claims = c.get("claims" as never) as AuthClaims | undefined;
367
+ const visibleBackends = claims
368
+ ? filterBackendsByClaims(backends, claims, config.backends)
369
+ : backends;
370
+
371
+ const sessionId = c.req.header("mcp-session-id");
372
+
373
+ // Reuse existing session
374
+ if (sessionId && sessions.has(sessionId)) {
375
+ const session = sessions.get(sessionId)!;
376
+ session.lastAccess = Date.now();
377
+ const response = await session.transport.handleRequest(c.req.raw);
378
+ return response;
272
379
  }
273
380
 
274
- const result = await verifier(token);
275
- if (result.isErr()) return c.json({ error: result.error }, 401);
381
+ // New session
382
+ const server = createMcpServer(visibleBackends);
383
+ const transport = new WebStandardStreamableHTTPServerTransport({
384
+ sessionIdGenerator: () => crypto.randomUUID(),
385
+ });
276
386
 
277
- // Store claims for per-backend filtering
278
- c.set("claims" as never, result.value as never);
279
- await next();
280
- });
281
- }
387
+ await server.connect(transport);
282
388
 
283
- // Session management for stateful MCP connections
284
- const sessions = new Map<
285
- string,
286
- {
287
- server: McpServer;
288
- transport: WebStandardStreamableHTTPServerTransport;
289
- lastAccess: number;
290
- }
291
- >();
292
- const sessionTtlMs = (config.sessionTtlMinutes ?? 30) * 60 * 1000;
293
-
294
- // Expire stale sessions every minute
295
- setInterval(() => {
296
- const now = Date.now();
297
- for (const [id, session] of sessions) {
298
- if (now - session.lastAccess > sessionTtlMs) {
299
- sessions.delete(id);
389
+ // Store session after first response (which contains the session ID)
390
+ const response = await transport.handleRequest(c.req.raw);
391
+
392
+ const newSessionId = response.headers.get("mcp-session-id");
393
+ if (newSessionId) {
394
+ sessions.set(newSessionId, { server, transport, lastAccess: Date.now() });
300
395
  }
301
- }
302
- }, 60_000);
303
-
304
- // MCP endpoint — Streamable HTTP with session support
305
- app.all("/mcp", async (c) => {
306
- // Resolve visible backends based on auth claims
307
- const claims = c.get("claims" as never) as AuthClaims | undefined;
308
- const visibleBackends = claims
309
- ? filterBackendsByClaims(backends, claims, config.backends)
310
- : backends;
311
-
312
- const sessionId = c.req.header("mcp-session-id");
313
-
314
- // Reuse existing session
315
- if (sessionId && sessions.has(sessionId)) {
316
- const session = sessions.get(sessionId)!;
317
- session.lastAccess = Date.now();
318
- const response = await session.transport.handleRequest(c.req.raw);
319
- return response;
320
- }
321
396
 
322
- // New session
323
- const server = createMcpServer(visibleBackends);
324
- const transport = new WebStandardStreamableHTTPServerTransport({
325
- sessionIdGenerator: () => crypto.randomUUID(),
397
+ return response;
326
398
  });
327
399
 
328
- await server.connect(transport);
400
+ // Graceful shutdown — disconnect all backend clients before exiting
401
+ async function shutdown(signal: string): Promise<void> {
402
+ console.log(`\nmcpx received ${signal}, shutting down...`);
403
+ const disconnects = Array.from(backends.values()).map((b) =>
404
+ b.client.close().catch((err: Error) => {
405
+ console.error(` failed to disconnect backend ${b.name}: ${err.message}`);
406
+ }),
407
+ );
408
+ await Promise.allSettled(disconnects);
409
+ console.log("mcpx shutdown complete");
410
+ process.exit(0);
411
+ }
329
412
 
330
- // Store session after first response (which contains the session ID)
331
- const response = await transport.handleRequest(c.req.raw);
413
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
414
+ process.on("SIGINT", () => shutdown("SIGINT"));
332
415
 
333
- const newSessionId = response.headers.get("mcp-session-id");
334
- if (newSessionId) {
335
- sessions.set(newSessionId, { server, transport, lastAccess: Date.now() });
336
- }
416
+ console.log(`\nmcpx listening on http://localhost:${config.port}`);
417
+ console.log(` MCP endpoint: http://localhost:${config.port}/mcp`);
418
+ console.log(` Health: http://localhost:${config.port}/health`);
337
419
 
338
- return response;
339
- });
340
-
341
- // Graceful shutdown — disconnect all backend clients before exiting
342
- async function shutdown(signal: string): Promise<void> {
343
- console.log(`\nmcpx received ${signal}, shutting down...`);
344
- const disconnects = Array.from(backends.values()).map((b) =>
345
- b.client.close().catch((err: Error) => {
346
- console.error(` failed to disconnect backend ${b.name}: ${err.message}`);
347
- }),
348
- );
349
- await Promise.allSettled(disconnects);
350
- console.log("mcpx shutdown complete");
351
- process.exit(0);
420
+ // Bun.serve compat — export default must be at module level but
421
+ // config/app are block-scoped, so we assign to module-level vars.
422
+ _exportPort = config.port;
423
+ _exportFetch = app.fetch;
352
424
  }
353
425
 
354
- process.on("SIGTERM", () => shutdown("SIGTERM"));
355
- process.on("SIGINT", () => shutdown("SIGINT"));
356
-
357
- console.log(`\nmcpx listening on http://localhost:${config.port}`);
358
- console.log(` MCP endpoint: http://localhost:${config.port}/mcp`);
359
- console.log(` Health: http://localhost:${config.port}/health`);
426
+ let _exportPort = 0;
427
+ let _exportFetch: (req: Request) => Response | Promise<Response> = () => new Response("stdio mode");
360
428
 
361
429
  export default {
362
- port: config.port,
363
- fetch: app.fetch,
430
+ get port() {
431
+ return _exportPort;
432
+ },
433
+ get fetch() {
434
+ return _exportFetch;
435
+ },
364
436
  };