mcpbox 0.0.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.
@@ -0,0 +1,341 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { validateToolName } from "@modelcontextprotocol/sdk/shared/toolNameValidation.js";
4
+ import { logger } from "../logger.js";
5
+ import { NAME, VERSION } from "../version.js";
6
+ import { namespaceName, stripNamespace } from "./namespace.js";
7
+ export class McpManager {
8
+ mcps = new Map();
9
+ toolToMcp = new Map(); // tool name -> mcp name
10
+ resourceToMcp = new Map(); // uri -> mcp name
11
+ promptToMcp = new Map(); // prompt name -> mcp name
12
+ mcpDebug;
13
+ useNamespacing = true;
14
+ constructor(logConfig) {
15
+ this.mcpDebug = logConfig?.mcpDebug ?? false;
16
+ }
17
+ async start(configs) {
18
+ if (configs.length === 0) {
19
+ logger.warn("No MCPs configured");
20
+ return;
21
+ }
22
+ //Can be disabled via internal env var for conformance test.
23
+ this.useNamespacing = process.env.__MCPBOX_SKIP_NAMESPACE !== "true";
24
+ logger.info({ count: configs.length, namespacing: this.useNamespacing }, "Starting MCPs");
25
+ let succeeded = 0;
26
+ let failed = 0;
27
+ for (const config of configs) {
28
+ try {
29
+ await this.startMcp(config);
30
+ succeeded++;
31
+ }
32
+ catch (error) {
33
+ failed++;
34
+ // Redact sensitive values from args for logging
35
+ const redactedArgs = config.args?.map((arg) => arg.replace(/(PASSWORD|SECRET|TOKEN|KEY|PIN)=.*/i, "$1=***"));
36
+ logger.error({
37
+ mcp: config.name,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ command: config.command,
40
+ args: redactedArgs,
41
+ }, `MCP failed to start: ${config.name}`);
42
+ }
43
+ }
44
+ if (failed > 0) {
45
+ logger.warn({ succeeded, failed, total: configs.length }, `MCPs started with failures: ${succeeded}/${configs.length}`);
46
+ }
47
+ else {
48
+ logger.info({ count: succeeded }, "All MCPs started successfully");
49
+ }
50
+ }
51
+ async startMcp(config) {
52
+ logger.debug({ mcp: config.name }, `Starting MCP: ${config.name}`);
53
+ const transport = new StdioClientTransport({
54
+ command: config.command,
55
+ args: config.args,
56
+ env: { ...getDefaultEnvironment(), ...config.env },
57
+ stderr: this.mcpDebug ? "pipe" : "ignore",
58
+ });
59
+ // Forward MCP stderr to our logger when debug is enabled
60
+ if (this.mcpDebug && transport.stderr) {
61
+ transport.stderr.on("data", (data) => {
62
+ const message = data.toString().trim();
63
+ if (message) {
64
+ logger.info(`[mcp:${config.name}] ${message}`);
65
+ }
66
+ });
67
+ }
68
+ const client = new Client({
69
+ name: NAME,
70
+ version: VERSION,
71
+ });
72
+ await client.connect(transport);
73
+ // Get tools from this MCP (filter by allowlist if configured, then namespace)
74
+ const { tools: rawTools } = await client.listTools();
75
+ const toolsAllowlist = config.tools;
76
+ const filteredTools = toolsAllowlist
77
+ ? rawTools.filter((tool) => toolsAllowlist.includes(tool.name))
78
+ : rawTools;
79
+ if (toolsAllowlist) {
80
+ const availableToolNames = rawTools.map((t) => t.name);
81
+ const unknownTools = toolsAllowlist.filter((name) => !availableToolNames.includes(name));
82
+ if (unknownTools.length > 0) {
83
+ logger.warn({
84
+ mcp: config.name,
85
+ unknownTools,
86
+ availableTools: availableToolNames,
87
+ }, "Tools allowlist contains unknown tool names (possible typo)");
88
+ }
89
+ logger.info({
90
+ mcp: config.name,
91
+ allowed: filteredTools.length,
92
+ total: rawTools.length,
93
+ }, "Filtered tools by allowlist");
94
+ }
95
+ const tools = this.useNamespacing
96
+ ? filteredTools.map((tool) => ({
97
+ ...tool,
98
+ name: namespaceName(config.name, tool.name),
99
+ }))
100
+ : filteredTools;
101
+ for (const tool of tools) {
102
+ const validation = validateToolName(tool.name);
103
+ if (!validation.isValid || validation.warnings.length > 0) {
104
+ logger.warn({ tool: tool.name, warnings: validation.warnings }, `Tool name may not comply with SEP-986: ${tool.name}`);
105
+ }
106
+ this.toolToMcp.set(tool.name, config.name);
107
+ }
108
+ // Get resources from this MCP (namespace them if multiple servers)
109
+ let resources = [];
110
+ try {
111
+ const { resources: rawResources } = await client.listResources();
112
+ resources = this.useNamespacing
113
+ ? rawResources.map((resource) => ({
114
+ ...resource,
115
+ uri: namespaceName(config.name, resource.uri),
116
+ }))
117
+ : rawResources;
118
+ for (const resource of resources) {
119
+ this.resourceToMcp.set(resource.uri, config.name);
120
+ }
121
+ }
122
+ catch {
123
+ logger.info({ mcp: config.name }, "Server doesn't support resources");
124
+ }
125
+ // Get prompts from this MCP (namespace them if multiple servers)
126
+ let prompts = [];
127
+ try {
128
+ const { prompts: rawPrompts } = await client.listPrompts();
129
+ prompts = this.useNamespacing
130
+ ? rawPrompts.map((prompt) => ({
131
+ ...prompt,
132
+ name: namespaceName(config.name, prompt.name),
133
+ }))
134
+ : rawPrompts;
135
+ for (const prompt of prompts) {
136
+ this.promptToMcp.set(prompt.name, config.name);
137
+ }
138
+ }
139
+ catch {
140
+ logger.info({ mcp: config.name }, "Server doesn't support prompts");
141
+ }
142
+ this.mcps.set(config.name, {
143
+ name: config.name,
144
+ client,
145
+ transport,
146
+ tools,
147
+ resources,
148
+ prompts,
149
+ });
150
+ logger.info({
151
+ mcp: config.name,
152
+ tools: tools.length,
153
+ resources: resources.length,
154
+ prompts: prompts.length,
155
+ }, `MCP ready: ${config.name}`);
156
+ }
157
+ async stop() {
158
+ const stopPromises = [];
159
+ for (const [name, mcp] of this.mcps) {
160
+ logger.info(`Stopping MCP: ${name}`);
161
+ stopPromises.push(mcp.transport.close().catch((error) => {
162
+ logger.error({
163
+ error: error instanceof Error ? error.message : String(error),
164
+ }, `Error stopping MCP: ${name}`);
165
+ }));
166
+ }
167
+ await Promise.all(stopPromises);
168
+ this.mcps.clear();
169
+ this.toolToMcp.clear();
170
+ this.resourceToMcp.clear();
171
+ this.promptToMcp.clear();
172
+ logger.info("All MCPs stopped");
173
+ }
174
+ get count() {
175
+ return this.mcps.size;
176
+ }
177
+ listTools() {
178
+ const allTools = [];
179
+ for (const mcp of this.mcps.values()) {
180
+ allTools.push(...mcp.tools);
181
+ }
182
+ return allTools;
183
+ }
184
+ listResources() {
185
+ const allResources = [];
186
+ for (const mcp of this.mcps.values()) {
187
+ allResources.push(...mcp.resources);
188
+ }
189
+ return allResources;
190
+ }
191
+ listPrompts() {
192
+ const allPrompts = [];
193
+ for (const mcp of this.mcps.values()) {
194
+ allPrompts.push(...mcp.prompts);
195
+ }
196
+ return allPrompts;
197
+ }
198
+ async checkHealth() {
199
+ const servers = {};
200
+ for (const [name, mcp] of this.mcps) {
201
+ try {
202
+ await mcp.client.ping();
203
+ servers[name] = {
204
+ status: "up",
205
+ tools: mcp.tools.length,
206
+ resources: mcp.resources.length,
207
+ prompts: mcp.prompts.length,
208
+ };
209
+ }
210
+ catch {
211
+ servers[name] = {
212
+ status: "down",
213
+ tools: mcp.tools.length,
214
+ resources: mcp.resources.length,
215
+ prompts: mcp.prompts.length,
216
+ };
217
+ }
218
+ }
219
+ return { servers };
220
+ }
221
+ async callTool(toolName, args) {
222
+ const mcpName = this.toolToMcp.get(toolName);
223
+ if (!mcpName) {
224
+ logger.warn(`Unknown tool called: ${toolName}`);
225
+ throw new Error(`Unknown tool: ${toolName}`);
226
+ }
227
+ const mcp = this.mcps.get(mcpName);
228
+ if (!mcp) {
229
+ logger.error({ mcpName }, `MCP not found for tool: ${toolName}`);
230
+ throw new Error(`MCP not found: ${mcpName}`);
231
+ }
232
+ // Strip namespace prefix to get original tool name
233
+ const originalName = this.useNamespacing
234
+ ? stripNamespace(mcpName, toolName)
235
+ : toolName;
236
+ logger.info({ args }, `Tool call: ${toolName}`);
237
+ const startTime = Date.now();
238
+ const result = await mcp.client.callTool({
239
+ name: originalName,
240
+ arguments: args,
241
+ });
242
+ const duration = Date.now() - startTime;
243
+ logger.info({
244
+ duration: `${duration}ms`,
245
+ isError: result.isError ?? false,
246
+ }, `Tool result: ${toolName}`);
247
+ return result;
248
+ }
249
+ async readResource(resourceUri) {
250
+ const mcpName = this.resourceToMcp.get(resourceUri);
251
+ if (!mcpName) {
252
+ logger.warn(`Unknown resource: ${resourceUri}`);
253
+ throw new Error(`Unknown resource: ${resourceUri}`);
254
+ }
255
+ const mcp = this.mcps.get(mcpName);
256
+ if (!mcp) {
257
+ logger.error({ mcpName }, `MCP not found for resource: ${resourceUri}`);
258
+ throw new Error(`MCP not found: ${mcpName}`);
259
+ }
260
+ // Strip namespace prefix to get original URI
261
+ const originalUri = this.useNamespacing
262
+ ? stripNamespace(mcpName, resourceUri)
263
+ : resourceUri;
264
+ logger.info(`Resource read: ${resourceUri}`);
265
+ const startTime = Date.now();
266
+ const result = await mcp.client.readResource({ uri: originalUri });
267
+ const duration = Date.now() - startTime;
268
+ logger.info({ duration: `${duration}ms` }, `Resource result: ${resourceUri}`);
269
+ return result;
270
+ }
271
+ async getPrompt(promptName, args) {
272
+ const mcpName = this.promptToMcp.get(promptName);
273
+ if (!mcpName) {
274
+ logger.warn(`Unknown prompt: ${promptName}`);
275
+ throw new Error(`Unknown prompt: ${promptName}`);
276
+ }
277
+ const mcp = this.mcps.get(mcpName);
278
+ if (!mcp) {
279
+ logger.error({ mcpName }, `MCP not found for prompt: ${promptName}`);
280
+ throw new Error(`MCP not found: ${mcpName}`);
281
+ }
282
+ // Strip namespace prefix to get original name
283
+ const originalName = this.useNamespacing
284
+ ? stripNamespace(mcpName, promptName)
285
+ : promptName;
286
+ logger.info({ args }, `Prompt get: ${promptName}`);
287
+ const startTime = Date.now();
288
+ const result = await mcp.client.getPrompt({
289
+ name: originalName,
290
+ arguments: args,
291
+ });
292
+ const duration = Date.now() - startTime;
293
+ logger.info({ duration: `${duration}ms` }, `Prompt result: ${promptName}`);
294
+ return result;
295
+ }
296
+ async complete(ref, argument) {
297
+ const { mcpName, originalRef } = this.resolveCompletionRef(ref);
298
+ const mcp = this.mcps.get(mcpName);
299
+ if (!mcp) {
300
+ logger.error({ mcpName }, "MCP not found for completion");
301
+ throw new Error(`MCP not found: ${mcpName}`);
302
+ }
303
+ logger.info({ ref, argument }, "Completion request");
304
+ const startTime = Date.now();
305
+ const result = await mcp.client.complete({ ref: originalRef, argument });
306
+ const duration = Date.now() - startTime;
307
+ logger.info({ duration: `${duration}ms` }, "Completion result");
308
+ return result;
309
+ }
310
+ resolveCompletionRef(ref) {
311
+ if (ref.type === "ref/prompt" && ref.name) {
312
+ const mcpName = this.promptToMcp.get(ref.name);
313
+ if (!mcpName) {
314
+ logger.warn(`Unknown prompt for completion: ${ref.name}`);
315
+ throw new Error(`Unknown prompt: ${ref.name}`);
316
+ }
317
+ const originalName = this.useNamespacing
318
+ ? stripNamespace(mcpName, ref.name)
319
+ : ref.name;
320
+ return {
321
+ mcpName,
322
+ originalRef: { type: "ref/prompt", name: originalName },
323
+ };
324
+ }
325
+ if (ref.type === "ref/resource" && ref.uri) {
326
+ const mcpName = this.resourceToMcp.get(ref.uri);
327
+ if (!mcpName) {
328
+ logger.warn(`Unknown resource for completion: ${ref.uri}`);
329
+ throw new Error(`Unknown resource: ${ref.uri}`);
330
+ }
331
+ const originalUri = this.useNamespacing
332
+ ? stripNamespace(mcpName, ref.uri)
333
+ : ref.uri;
334
+ return {
335
+ mcpName,
336
+ originalRef: { type: "ref/resource", uri: originalUri },
337
+ };
338
+ }
339
+ throw new Error(`Invalid completion ref type: ${ref.type}`);
340
+ }
341
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * MCP namespace utilities for prefixing and stripping server names
3
+ * from tool names, resource URIs, and prompt names.
4
+ *
5
+ * Format: `serverName__originalName`
6
+ */
7
+ /**
8
+ * Add namespace prefix to a name.
9
+ * @example namespaceName("github", "create_issue") => "github__create_issue"
10
+ */
11
+ export declare function namespaceName(serverName: string, name: string): string;
12
+ /**
13
+ * Extract server name from a namespaced name.
14
+ * Returns null if the name doesn't contain the separator.
15
+ * @example extractServerName("github__create_issue") => "github"
16
+ * @example extractServerName("create_issue") => null
17
+ */
18
+ export declare function extractServerName(namespacedName: string): string | null;
19
+ /**
20
+ * Strip namespace prefix to get the original name.
21
+ * @example stripNamespace("github", "github__create_issue") => "create_issue"
22
+ */
23
+ export declare function stripNamespace(serverName: string, namespacedName: string): string;
24
+ /**
25
+ * Check if a name is namespaced (contains the separator).
26
+ * @example isNamespaced("github__create_issue") => true
27
+ * @example isNamespaced("create_issue") => false
28
+ */
29
+ export declare function isNamespaced(name: string): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * MCP namespace utilities for prefixing and stripping server names
3
+ * from tool names, resource URIs, and prompt names.
4
+ *
5
+ * Format: `serverName__originalName`
6
+ */
7
+ const SEPARATOR = "__";
8
+ /**
9
+ * Add namespace prefix to a name.
10
+ * @example namespaceName("github", "create_issue") => "github__create_issue"
11
+ */
12
+ export function namespaceName(serverName, name) {
13
+ return `${serverName}${SEPARATOR}${name}`;
14
+ }
15
+ /**
16
+ * Extract server name from a namespaced name.
17
+ * Returns null if the name doesn't contain the separator.
18
+ * @example extractServerName("github__create_issue") => "github"
19
+ * @example extractServerName("create_issue") => null
20
+ */
21
+ export function extractServerName(namespacedName) {
22
+ const idx = namespacedName.indexOf(SEPARATOR);
23
+ return idx > 0 ? namespacedName.substring(0, idx) : null;
24
+ }
25
+ /**
26
+ * Strip namespace prefix to get the original name.
27
+ * @example stripNamespace("github", "github__create_issue") => "create_issue"
28
+ */
29
+ export function stripNamespace(serverName, namespacedName) {
30
+ return namespacedName.substring(serverName.length + SEPARATOR.length);
31
+ }
32
+ /**
33
+ * Check if a name is namespaced (contains the separator).
34
+ * @example isNamespaced("github__create_issue") => true
35
+ * @example isNamespaced("create_issue") => false
36
+ */
37
+ export function isNamespaced(name) {
38
+ return name.includes(SEPARATOR);
39
+ }
@@ -0,0 +1,7 @@
1
+ import type { Config } from "./config/types.js";
2
+ import { McpManager } from "./mcp/manager.js";
3
+ export declare function createServer(config: Config): Promise<{
4
+ server: import("@hono/node-server").ServerType;
5
+ mcpManager: McpManager;
6
+ close(): Promise<void>;
7
+ }>;
package/dist/server.js ADDED
@@ -0,0 +1,246 @@
1
+ import path from "node:path";
2
+ import { serve } from "@hono/node-server";
3
+ import { Hono } from "hono";
4
+ import { LOGO_PNG_BASE64 } from "./assets.js";
5
+ import { checkApiKey } from "./auth/apikey.js";
6
+ import { OAuthServer } from "./auth/oauth.js";
7
+ import { logger } from "./logger.js";
8
+ import { handleCompletionComplete, handleInitialize, handleInitialized, handleMethodNotFound, handlePing, handlePromptsGet, handlePromptsList, handleResourcesList, handleResourcesRead, handleToolsCall, handleToolsList, } from "./mcp/handlers.js";
9
+ import { McpManager } from "./mcp/manager.js";
10
+ import { MemoryStore } from "./storage/memory.js";
11
+ import { SqliteStore } from "./storage/sqlite.js";
12
+ const LOGO_PNG = Buffer.from(LOGO_PNG_BASE64, "base64");
13
+ async function createStore(config) {
14
+ if (config.storage?.type === "sqlite") {
15
+ const dbPath = config.storage.path ?? path.join(process.cwd(), "data", "mcpbox.db");
16
+ return SqliteStore.create(dbPath);
17
+ }
18
+ return new MemoryStore();
19
+ }
20
+ export async function createServer(config) {
21
+ // Start MCP servers
22
+ const mcpManager = new McpManager(config.log);
23
+ await mcpManager.start(config.mcps);
24
+ // Set up authentication
25
+ const auth = config.auth;
26
+ let apiKey;
27
+ let oauthServer = null;
28
+ let store = null;
29
+ if (auth?.type === "apikey") {
30
+ apiKey = auth.apiKey;
31
+ }
32
+ else if (auth?.type === "oauth") {
33
+ store = await createStore(config);
34
+ oauthServer = new OAuthServer({
35
+ issuer: auth.issuer ?? `http://localhost:${config.server.port}`,
36
+ users: auth.users,
37
+ clients: auth.clients,
38
+ dynamicRegistration: auth.dynamic_registration,
39
+ }, store);
40
+ }
41
+ // Warn if storage is configured but not used
42
+ if (config.storage && auth?.type !== "oauth") {
43
+ logger.warn("Storage config ignored: only used with OAuth authentication");
44
+ }
45
+ const app = new Hono();
46
+ // Request logging middleware (applies to all routes)
47
+ app.use("*", async (c, next) => {
48
+ const start = Date.now();
49
+ const method = c.req.method;
50
+ const pathname = new URL(c.req.url).pathname;
51
+ logger.debug({ method, path: pathname }, "Request received");
52
+ await next();
53
+ const duration = Date.now() - start;
54
+ const status = c.res.status;
55
+ logger.debug({ method, path: pathname, status, duration: `${duration}ms` }, "Request completed");
56
+ });
57
+ // --- Public routes (no auth required) ---
58
+ app.get("/health", (c) => c.json({ status: "ok" }));
59
+ app.get("/logo.png", (c) => {
60
+ c.header("Content-Type", "image/png");
61
+ return c.body(LOGO_PNG);
62
+ });
63
+ app.get("/favicon.ico", (c) => {
64
+ c.header("Content-Type", "image/png");
65
+ return c.body(LOGO_PNG);
66
+ });
67
+ app.get("/icon.png", (c) => {
68
+ c.header("Content-Type", "image/png");
69
+ return c.body(LOGO_PNG);
70
+ });
71
+ app.get("/favicon.png", (c) => {
72
+ c.header("Content-Type", "image/png");
73
+ return c.body(LOGO_PNG);
74
+ });
75
+ if (oauthServer) {
76
+ // RFC 9728: Protected Resource Metadata
77
+ app.get("/.well-known/oauth-protected-resource", (c) => {
78
+ return c.json(oauthServer.getProtectedResourceMetadata());
79
+ });
80
+ // RFC 8414: Authorization Server Metadata
81
+ app.get("/.well-known/oauth-authorization-server", (c) => {
82
+ return c.json(oauthServer.getAuthorizationServerMetadata());
83
+ });
84
+ // Authorization endpoint
85
+ app.get("/authorize", async (c) => {
86
+ const query = new URL(c.req.url).searchParams;
87
+ return oauthServer.handleAuthorize(c, query);
88
+ });
89
+ app.post("/authorize", async (c) => {
90
+ const query = new URL(c.req.url).searchParams;
91
+ const body = await c.req.text();
92
+ return oauthServer.handleAuthorize(c, query, body);
93
+ });
94
+ // Token endpoint
95
+ app.post("/token", async (c) => {
96
+ const body = await c.req.text();
97
+ return oauthServer.handleToken(c, body);
98
+ });
99
+ // Dynamic Client Registration endpoint (RFC 7591)
100
+ app.post("/register", async (c) => {
101
+ const body = await c.req.text();
102
+ return oauthServer.handleRegister(c, body);
103
+ });
104
+ }
105
+ // --- Protected routes (auth required) ---
106
+ const protectedRoutes = new Hono();
107
+ // Auth middleware for protected routes
108
+ protectedRoutes.use("*", async (c, next) => {
109
+ if (apiKey) {
110
+ const providedKey = c.req.header("x-api-key") ??
111
+ c.req.header("authorization")?.replace(/^(Bearer|ApiKey)\s+/i, "");
112
+ if (!checkApiKey(providedKey, apiKey)) {
113
+ return c.json({ error: "Unauthorized: Invalid or missing API key" }, 401);
114
+ }
115
+ }
116
+ else if (oauthServer) {
117
+ const authHeader = c.req.header("authorization");
118
+ const validation = oauthServer.validateToken(authHeader);
119
+ if (!validation.valid) {
120
+ return oauthServer.sendUnauthorized(c, validation.error);
121
+ }
122
+ logger.debug({ userId: validation.userId }, "OAuth token validated");
123
+ }
124
+ return next();
125
+ });
126
+ // MCP handler
127
+ const handleMcp = async (c) => {
128
+ let message;
129
+ try {
130
+ message = await c.req.json();
131
+ }
132
+ catch (e) {
133
+ logger.warn({ error: e instanceof Error ? e.message : String(e) }, "MCP parse error");
134
+ return c.json({
135
+ jsonrpc: "2.0",
136
+ error: { code: -32700, message: "Parse error" },
137
+ id: null,
138
+ }, 400);
139
+ }
140
+ const method = "method" in message ? message.method : undefined;
141
+ const id = "id" in message ? message.id : undefined;
142
+ logger.debug({ method, id }, "MCP request");
143
+ if (method === "initialize") {
144
+ return handleInitialize(c, message);
145
+ }
146
+ if (method === "notifications/initialized") {
147
+ return handleInitialized(c);
148
+ }
149
+ if (method === "tools/list") {
150
+ return handleToolsList(c, message, mcpManager);
151
+ }
152
+ if (method === "tools/call") {
153
+ const params = message.params;
154
+ return handleToolsCall(c, message, mcpManager, params);
155
+ }
156
+ if (method === "resources/list") {
157
+ return handleResourcesList(c, message, mcpManager);
158
+ }
159
+ if (method === "resources/read") {
160
+ const params = message.params;
161
+ return handleResourcesRead(c, message, mcpManager, params);
162
+ }
163
+ if (method === "prompts/list") {
164
+ return handlePromptsList(c, message, mcpManager);
165
+ }
166
+ if (method === "prompts/get") {
167
+ const params = message.params;
168
+ return handlePromptsGet(c, message, mcpManager, params);
169
+ }
170
+ if (method === "ping") {
171
+ return handlePing(c, message);
172
+ }
173
+ if (method === "completion/complete") {
174
+ const params = message.params;
175
+ return handleCompletionComplete(c, message, mcpManager, params);
176
+ }
177
+ return handleMethodNotFound(c, message, method ?? "unknown");
178
+ };
179
+ // Status endpoint (protected)
180
+ protectedRoutes.get("/status", async (c) => {
181
+ const health = await mcpManager.checkHealth();
182
+ return c.json({ servers: health.servers });
183
+ });
184
+ // MCP endpoints (protected)
185
+ protectedRoutes.post("/", handleMcp);
186
+ protectedRoutes.post("/mcp", handleMcp);
187
+ // Mount protected routes
188
+ app.route("/", protectedRoutes);
189
+ // 404 for other routes
190
+ app.notFound((c) => c.json({ error: "Not found" }, 404));
191
+ // Error handler
192
+ app.onError((err, c) => {
193
+ logger.error({ error: err.message }, "Server error");
194
+ return c.json({ error: "Internal server error" }, 500);
195
+ });
196
+ const server = serve({
197
+ fetch: app.fetch,
198
+ port: config.server.port,
199
+ hostname: "0.0.0.0",
200
+ }, () => {
201
+ // Log authentication configuration
202
+ if (!auth) {
203
+ logger.warn("No authentication configured");
204
+ }
205
+ else if (auth.type === "apikey") {
206
+ logger.info("Authentication: API key");
207
+ }
208
+ else if (auth.type === "oauth") {
209
+ const userCount = auth.users?.length ?? 0;
210
+ const clientCount = auth.clients?.length ?? 0;
211
+ const dynamicReg = auth.dynamic_registration ? "enabled" : "disabled";
212
+ logger.info({
213
+ users: userCount,
214
+ clients: clientCount,
215
+ dynamicRegistration: dynamicReg,
216
+ }, "Authentication: OAuth");
217
+ }
218
+ logger.info({ port: config.server.port, mcpCount: mcpManager.count }, `mcpbox listening on port ${config.server.port}`);
219
+ });
220
+ return {
221
+ server,
222
+ mcpManager,
223
+ async close() {
224
+ logger.info("Shutting down mcpbox...");
225
+ await mcpManager.stop();
226
+ if (oauthServer) {
227
+ oauthServer.close();
228
+ }
229
+ if (store) {
230
+ await store.close();
231
+ }
232
+ return new Promise((resolve, reject) => {
233
+ server.close((err) => {
234
+ if (err) {
235
+ logger.error({ error: err.message }, "Error closing server");
236
+ reject(err);
237
+ }
238
+ else {
239
+ logger.info("Server closed");
240
+ resolve();
241
+ }
242
+ });
243
+ });
244
+ },
245
+ };
246
+ }
@@ -0,0 +1,20 @@
1
+ import type { StateStore, StoredAccessToken, StoredClient, StoredRefreshToken } from "./types.js";
2
+ export declare class MemoryStore implements StateStore {
3
+ private clients;
4
+ private accessTokens;
5
+ private refreshTokens;
6
+ constructor();
7
+ getClient(clientId: string): StoredClient | null;
8
+ saveClient(client: StoredClient): void;
9
+ deleteClient(clientId: string): void;
10
+ getAllDynamicClients(): StoredClient[];
11
+ getAccessToken(token: string): StoredAccessToken | null;
12
+ saveAccessToken(token: StoredAccessToken): void;
13
+ deleteAccessToken(token: string): void;
14
+ getRefreshToken(token: string): StoredRefreshToken | null;
15
+ saveRefreshToken(token: StoredRefreshToken): void;
16
+ deleteRefreshToken(token: string): void;
17
+ rotateRefreshToken(oldTokenHash: string, newToken: StoredRefreshToken): void;
18
+ cleanupExpired(): void;
19
+ close(): void;
20
+ }