mcpbox 0.2.0 → 0.2.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/README.md CHANGED
@@ -5,14 +5,18 @@
5
5
  </picture>
6
6
  </p>
7
7
 
8
- **MCPBox** is a lightweight gateway that exposes local stdio-based MCP (Model Context Protocol) servers via Streamable HTTP, enabling Claude and other AI agents to connect from anywhere.
8
+ **MCPBox** is a lightweight gateway that exposes local stdio-based [MCP](https://modelcontextprotocol.io) servers via Streamable HTTP, enabling Claude and other AI agents to connect from anywhere.
9
9
 
10
- - Aggregate multiple local stdio servers behind a single HTTP endpoint
11
- - Exposes Tools, Resources & Prompts
12
- - Namespaces with `servername__` prefix to avoid collisions (e.g., `github__create_issue`)
10
+ - Runs multiple servers behind a single HTTP endpoint
11
+ - Supports Tools, Resources & Prompts
12
+ - Namespaces with `servername__` prefix to avoid collisions
13
+ - Per-server tool filtering to limit AI access and reduce context usage
13
14
  - OAuth or API key authentication
14
15
 
15
- ![mcpbox diagram](assets/diagram.excalidraw.png)
16
+ <picture>
17
+ <source media="(prefers-color-scheme: dark)" srcset="assets/diagram-dark.excalidraw.png">
18
+ <img src="assets/diagram.excalidraw.png" alt="mcpbox diagram">
19
+ </picture>
16
20
 
17
21
  ## Quick Start
18
22
 
@@ -70,10 +74,10 @@ See [`mcpbox.example.jsonc`](mcpbox.example.jsonc) for all options. All string v
70
74
 
71
75
  To expose MCPBox remotely, put it behind a TLS-terminating reverse proxy.
72
76
 
73
- Before deploying:
74
- - [ ] Use `storage.type: "sqlite"` for persistence across restarts
75
- - [ ] Set `auth.issuer` to your public URL when using OAuth
76
- - [ ] Use bcrypt hashes for passwords
77
+ Before deploying with OAuth:
78
+ - [ ] Use sqlite storage for persistence across restarts
79
+ - [ ] Set issuer to your public URL
80
+ - [ ] Use bcrypt hashes for local passwords
77
81
 
78
82
  > [!NOTE]
79
83
  > MCPBox is single-instance only — don't run multiple instances behind a load balancer.
@@ -93,7 +97,9 @@ Then update your config with the generated public URL:
93
97
  "auth": {
94
98
  "type": "oauth",
95
99
  "issuer": "https://<tunnel-id>.trycloudflare.com",
96
- "users": [{ "username": "admin", "password": "${MCPBOX_PASSWORD}" }],
100
+ "identityProviders": [
101
+ { "type": "local", "users": [{ "username": "admin", "password": "${MCPBOX_PASSWORD}" }] }
102
+ ],
97
103
  "dynamicRegistration": true
98
104
  },
99
105
  "storage": {
@@ -126,12 +132,28 @@ claude mcp add --transport http mcpbox https://your-mcpbox-url.com
126
132
 
127
133
  Requires `dynamicRegistration: true` in your config.
128
134
 
129
- ### MCP clients with JSON config
135
+ ### Other MCP clients
136
+
137
+ **With dynamic registration (OAuth)** — just provide the URL:
130
138
 
131
139
  ```json
132
140
  {
133
141
  "mcpServers": {
134
142
  "mcpbox": {
143
+ "type": "http",
144
+ "url": "https://your-mcpbox-url.com"
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ **With API key:**
151
+
152
+ ```json
153
+ {
154
+ "mcpServers": {
155
+ "mcpbox": {
156
+ "type": "http",
135
157
  "url": "https://your-mcpbox-url.com",
136
158
  "headers": {
137
159
  "Authorization": "Bearer YOUR_API_KEY"
@@ -63,7 +63,7 @@ export class OAuthServer {
63
63
  return {
64
64
  resource: this.config.issuer,
65
65
  authorization_servers: [this.config.issuer],
66
- scopes_supported: ["mcp:tools"],
66
+ scopes_supported: ["mcp"],
67
67
  bearer_methods_supported: ["header"],
68
68
  // Non-standard: logo for client display
69
69
  logo_uri: `${this.config.issuer}/logo.png`,
@@ -87,7 +87,7 @@ export class OAuthServer {
87
87
  token_endpoint: `${this.config.issuer}/token`,
88
88
  grant_types_supported: grantTypes,
89
89
  token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
90
- scopes_supported: ["mcp:tools"],
90
+ scopes_supported: ["mcp"],
91
91
  };
92
92
  // Only advertise authorization endpoint if identity providers are configured
93
93
  if (hasProviders) {
@@ -656,7 +656,7 @@ export class OAuthServer {
656
656
  this.store.saveAccessToken({
657
657
  token: hashSecret(accessToken),
658
658
  clientId,
659
- scope: "mcp:tools",
659
+ scope: "mcp",
660
660
  expiresAt: Date.now() + expiresIn * 1000,
661
661
  userId: `client:${clientId}`, // Mark as client-authenticated
662
662
  });
@@ -666,7 +666,7 @@ export class OAuthServer {
666
666
  access_token: accessToken, // Return unhashed token to client
667
667
  token_type: "Bearer",
668
668
  expires_in: expiresIn,
669
- scope: "mcp:tools",
669
+ scope: "mcp",
670
670
  });
671
671
  }
672
672
  // RFC 6749 Section 4.1: Authorization Code Grant
@@ -129,6 +129,14 @@ export const AuthConfigSchema = z.discriminatedUnion("type", [
129
129
  return oauth.identityProviders && oauth.identityProviders.length > 0;
130
130
  }, {
131
131
  message: "dynamic registration requires identity providers to be configured for user login",
132
+ })
133
+ .refine((oauth) => {
134
+ if (!oauth.clients)
135
+ return true;
136
+ const ids = oauth.clients.map((c) => c.clientId);
137
+ return new Set(ids).size === ids.length;
138
+ }, {
139
+ message: "Duplicate client IDs are not allowed",
132
140
  }),
133
141
  ]);
134
142
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync } from "node:fs";
2
3
  import { loadConfig, resolveConfigPath, } from "./config/loader.js";
3
4
  import { configureLogger, logger } from "./logger.js";
4
5
  import { createServer } from "./server.js";
@@ -33,10 +34,6 @@ function parseArgs(args) {
33
34
  else if (arg === "-c" || arg === "--config") {
34
35
  result.config = args[++i];
35
36
  }
36
- else if (!arg.startsWith("-")) {
37
- // Positional arg = config path (backwards compat)
38
- result.config = arg;
39
- }
40
37
  }
41
38
  return result;
42
39
  }
@@ -50,6 +47,10 @@ if (args.version) {
50
47
  process.exit(0);
51
48
  }
52
49
  const configPath = resolveConfigPath(args.config);
50
+ if (args.config && !existsSync(configPath)) {
51
+ console.error(`Config file not found: ${configPath}`);
52
+ process.exit(1);
53
+ }
53
54
  let config;
54
55
  let warnings;
55
56
  try {
@@ -57,7 +58,6 @@ try {
57
58
  }
58
59
  catch (error) {
59
60
  const message = error instanceof Error ? error.message : String(error);
60
- // Log to stderr directly since logger config isn't loaded yet
61
61
  console.error(`Failed to load config from ${configPath}:\n${message}`);
62
62
  process.exit(1);
63
63
  }
@@ -0,0 +1,22 @@
1
+ import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { Context } from "hono";
3
+ import type { McpManager } from "./manager.js";
4
+ export declare function createMcpHandler(mcpManager: McpManager): (c: Context) => Promise<(Response & import("hono").TypedResponse<{
5
+ jsonrpc: "2.0";
6
+ id: string | number;
7
+ error: {
8
+ code: number;
9
+ message: string;
10
+ };
11
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
12
+ jsonrpc: "2.0";
13
+ id: string | number;
14
+ result: import("hono/utils/types").JSONValue;
15
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
16
+ jsonrpc: string;
17
+ error: {
18
+ code: ErrorCode;
19
+ message: string;
20
+ };
21
+ id: null;
22
+ }, 400, "json">) | (Response & import("hono").TypedResponse<null, 202, "body">)>;
@@ -0,0 +1,148 @@
1
+ import { CallToolRequestSchema, CompleteRequestSchema, ErrorCode, GetPromptRequestSchema, InitializeRequestSchema, isJSONRPCNotification, JSONRPCRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, PingRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { logger } from "../logger.js";
3
+ import { NAME, VERSION } from "../version.js";
4
+ function jsonrpcResult(id, result) {
5
+ return { jsonrpc: "2.0", id, result };
6
+ }
7
+ function jsonrpcError(id, code, message) {
8
+ return { jsonrpc: "2.0", id, error: { code, message } };
9
+ }
10
+ export function createMcpHandler(mcpManager) {
11
+ return async (c) => {
12
+ let body;
13
+ try {
14
+ body = await c.req.json();
15
+ }
16
+ catch (e) {
17
+ logger.warn({ error: e instanceof Error ? e.message : String(e) }, "MCP parse error");
18
+ return c.json({
19
+ jsonrpc: "2.0",
20
+ error: { code: ErrorCode.ParseError, message: "Parse error" },
21
+ id: null,
22
+ }, 400);
23
+ }
24
+ if (isJSONRPCNotification(body)) {
25
+ const method = body.method;
26
+ logger.debug({ method }, "MCP notification");
27
+ return c.body(null, 202);
28
+ }
29
+ const envelope = JSONRPCRequestSchema.safeParse(body);
30
+ if (!envelope.success) {
31
+ return c.json({
32
+ jsonrpc: "2.0",
33
+ error: {
34
+ code: ErrorCode.InvalidRequest,
35
+ message: "Invalid request",
36
+ },
37
+ id: null,
38
+ }, 400);
39
+ }
40
+ const request = envelope.data;
41
+ logger.debug({ method: request.method, id: request.id }, "MCP request");
42
+ return handleRequest(c, request, mcpManager);
43
+ };
44
+ }
45
+ async function handleRequest(c, request, mcpManager) {
46
+ const { id, method } = request;
47
+ if (method === "initialize") {
48
+ const parsed = InitializeRequestSchema.safeParse(request);
49
+ if (!parsed.success) {
50
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
51
+ }
52
+ const result = {
53
+ protocolVersion: "2025-11-25",
54
+ capabilities: {
55
+ tools: { listChanged: true },
56
+ resources: { listChanged: true },
57
+ prompts: { listChanged: true },
58
+ completions: {},
59
+ },
60
+ serverInfo: {
61
+ name: NAME,
62
+ version: VERSION,
63
+ },
64
+ };
65
+ return c.json(jsonrpcResult(id, result));
66
+ }
67
+ if (method === "ping") {
68
+ const parsed = PingRequestSchema.safeParse(request);
69
+ if (!parsed.success) {
70
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
71
+ }
72
+ return c.json(jsonrpcResult(id, {}));
73
+ }
74
+ if (method === "tools/list") {
75
+ const parsed = ListToolsRequestSchema.safeParse(request);
76
+ if (!parsed.success) {
77
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
78
+ }
79
+ return c.json(jsonrpcResult(id, { tools: mcpManager.listTools() }));
80
+ }
81
+ if (method === "tools/call") {
82
+ const parsed = CallToolRequestSchema.safeParse(request);
83
+ if (!parsed.success) {
84
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
85
+ }
86
+ try {
87
+ const result = await mcpManager.callTool(parsed.data.params);
88
+ return c.json(jsonrpcResult(id, result));
89
+ }
90
+ catch (error) {
91
+ return c.json(jsonrpcError(id, ErrorCode.InternalError, error instanceof Error ? error.message : "Internal error"));
92
+ }
93
+ }
94
+ if (method === "resources/list") {
95
+ const parsed = ListResourcesRequestSchema.safeParse(request);
96
+ if (!parsed.success) {
97
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
98
+ }
99
+ return c.json(jsonrpcResult(id, { resources: mcpManager.listResources() }));
100
+ }
101
+ if (method === "resources/read") {
102
+ const parsed = ReadResourceRequestSchema.safeParse(request);
103
+ if (!parsed.success) {
104
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
105
+ }
106
+ try {
107
+ const result = await mcpManager.readResource(parsed.data.params);
108
+ return c.json(jsonrpcResult(id, result));
109
+ }
110
+ catch (error) {
111
+ return c.json(jsonrpcError(id, ErrorCode.InternalError, error instanceof Error ? error.message : "Internal error"));
112
+ }
113
+ }
114
+ if (method === "prompts/list") {
115
+ const parsed = ListPromptsRequestSchema.safeParse(request);
116
+ if (!parsed.success) {
117
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
118
+ }
119
+ return c.json(jsonrpcResult(id, { prompts: mcpManager.listPrompts() }));
120
+ }
121
+ if (method === "prompts/get") {
122
+ const parsed = GetPromptRequestSchema.safeParse(request);
123
+ if (!parsed.success) {
124
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
125
+ }
126
+ try {
127
+ const result = await mcpManager.getPrompt(parsed.data.params);
128
+ return c.json(jsonrpcResult(id, result));
129
+ }
130
+ catch (error) {
131
+ return c.json(jsonrpcError(id, ErrorCode.InternalError, error instanceof Error ? error.message : "Internal error"));
132
+ }
133
+ }
134
+ if (method === "completion/complete") {
135
+ const parsed = CompleteRequestSchema.safeParse(request);
136
+ if (!parsed.success) {
137
+ return c.json(jsonrpcError(id, ErrorCode.InvalidParams, "Invalid params"));
138
+ }
139
+ try {
140
+ const result = await mcpManager.complete(parsed.data.params);
141
+ return c.json(jsonrpcResult(id, result));
142
+ }
143
+ catch (error) {
144
+ return c.json(jsonrpcError(id, ErrorCode.InternalError, error instanceof Error ? error.message : "Internal error"));
145
+ }
146
+ }
147
+ return c.json(jsonrpcError(id, ErrorCode.MethodNotFound, `Method not found: ${method}`));
148
+ }
@@ -1,6 +1,6 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
- import type { CallToolResult, CompleteResult, GetPromptResult, Prompt, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { CallToolRequestParams, CallToolResult, CompleteRequestParams, CompleteResult, GetPromptRequestParams, GetPromptResult, Prompt, ReadResourceRequestParams, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
4
4
  import type { LogConfig, McpConfig } from "../config/types.js";
5
5
  export interface ManagedMcp {
6
6
  name: string;
@@ -33,16 +33,9 @@ export declare class McpManager {
33
33
  prompts: number;
34
34
  }>;
35
35
  }>;
36
- callTool(toolName: string, args: Record<string, unknown>): Promise<CallToolResult>;
37
- readResource(resourceUri: string): Promise<ReadResourceResult>;
38
- getPrompt(promptName: string, args?: Record<string, string>): Promise<GetPromptResult>;
39
- complete(ref: {
40
- type: string;
41
- name?: string;
42
- uri?: string;
43
- }, argument: {
44
- name: string;
45
- value: string;
46
- }): Promise<CompleteResult>;
36
+ callTool(params: CallToolRequestParams): Promise<CallToolResult>;
37
+ readResource(params: ReadResourceRequestParams): Promise<ReadResourceResult>;
38
+ getPrompt(params: GetPromptRequestParams): Promise<GetPromptResult>;
39
+ complete(params: CompleteRequestParams): Promise<CompleteResult>;
47
40
  private resolveCompletionRef;
48
41
  }
@@ -217,91 +217,94 @@ export class McpManager {
217
217
  }
218
218
  return { servers };
219
219
  }
220
- async callTool(toolName, args) {
221
- const mcpName = this.toolToMcp.get(toolName);
220
+ async callTool(params) {
221
+ const mcpName = this.toolToMcp.get(params.name);
222
222
  if (!mcpName) {
223
- logger.warn(`Unknown tool called: ${toolName}`);
224
- throw new Error(`Unknown tool: ${toolName}`);
223
+ logger.warn(`Unknown tool called: ${params.name}`);
224
+ throw new Error(`Unknown tool: ${params.name}`);
225
225
  }
226
226
  const mcp = this.mcps.get(mcpName);
227
227
  if (!mcp) {
228
- logger.error({ mcpName }, `MCP not found for tool: ${toolName}`);
228
+ logger.error({ mcpName }, `MCP not found for tool: ${params.name}`);
229
229
  throw new Error(`MCP not found: ${mcpName}`);
230
230
  }
231
231
  // Strip namespace prefix to get original tool name
232
232
  const originalName = this.useNamespacing
233
- ? stripNamespace(mcpName, toolName)
234
- : toolName;
235
- logger.info({ args }, `Tool call: ${toolName}`);
233
+ ? stripNamespace(mcpName, params.name)
234
+ : params.name;
235
+ logger.info({ arguments: params.arguments }, `Tool call: ${params.name}`);
236
236
  const startTime = Date.now();
237
237
  const result = await mcp.client.callTool({
238
238
  name: originalName,
239
- arguments: args,
239
+ arguments: params.arguments,
240
240
  });
241
241
  const duration = Date.now() - startTime;
242
242
  logger.info({
243
243
  duration: `${duration}ms`,
244
244
  isError: result.isError ?? false,
245
- }, `Tool result: ${toolName}`);
245
+ }, `Tool result: ${params.name}`);
246
246
  return result;
247
247
  }
248
- async readResource(resourceUri) {
249
- const mcpName = this.resourceToMcp.get(resourceUri);
248
+ async readResource(params) {
249
+ const mcpName = this.resourceToMcp.get(params.uri);
250
250
  if (!mcpName) {
251
- logger.warn(`Unknown resource: ${resourceUri}`);
252
- throw new Error(`Unknown resource: ${resourceUri}`);
251
+ logger.warn(`Unknown resource: ${params.uri}`);
252
+ throw new Error(`Unknown resource: ${params.uri}`);
253
253
  }
254
254
  const mcp = this.mcps.get(mcpName);
255
255
  if (!mcp) {
256
- logger.error({ mcpName }, `MCP not found for resource: ${resourceUri}`);
256
+ logger.error({ mcpName }, `MCP not found for resource: ${params.uri}`);
257
257
  throw new Error(`MCP not found: ${mcpName}`);
258
258
  }
259
259
  // Strip namespace prefix to get original URI
260
260
  const originalUri = this.useNamespacing
261
- ? stripNamespace(mcpName, resourceUri)
262
- : resourceUri;
263
- logger.info(`Resource read: ${resourceUri}`);
261
+ ? stripNamespace(mcpName, params.uri)
262
+ : params.uri;
263
+ logger.info(`Resource read: ${params.uri}`);
264
264
  const startTime = Date.now();
265
265
  const result = await mcp.client.readResource({ uri: originalUri });
266
266
  const duration = Date.now() - startTime;
267
- logger.info({ duration: `${duration}ms` }, `Resource result: ${resourceUri}`);
267
+ logger.info({ duration: `${duration}ms` }, `Resource result: ${params.uri}`);
268
268
  return result;
269
269
  }
270
- async getPrompt(promptName, args) {
271
- const mcpName = this.promptToMcp.get(promptName);
270
+ async getPrompt(params) {
271
+ const mcpName = this.promptToMcp.get(params.name);
272
272
  if (!mcpName) {
273
- logger.warn(`Unknown prompt: ${promptName}`);
274
- throw new Error(`Unknown prompt: ${promptName}`);
273
+ logger.warn(`Unknown prompt: ${params.name}`);
274
+ throw new Error(`Unknown prompt: ${params.name}`);
275
275
  }
276
276
  const mcp = this.mcps.get(mcpName);
277
277
  if (!mcp) {
278
- logger.error({ mcpName }, `MCP not found for prompt: ${promptName}`);
278
+ logger.error({ mcpName }, `MCP not found for prompt: ${params.name}`);
279
279
  throw new Error(`MCP not found: ${mcpName}`);
280
280
  }
281
281
  // Strip namespace prefix to get original name
282
282
  const originalName = this.useNamespacing
283
- ? stripNamespace(mcpName, promptName)
284
- : promptName;
285
- logger.info({ args }, `Prompt get: ${promptName}`);
283
+ ? stripNamespace(mcpName, params.name)
284
+ : params.name;
285
+ logger.info({ arguments: params.arguments }, `Prompt get: ${params.name}`);
286
286
  const startTime = Date.now();
287
287
  const result = await mcp.client.getPrompt({
288
288
  name: originalName,
289
- arguments: args,
289
+ arguments: params.arguments,
290
290
  });
291
291
  const duration = Date.now() - startTime;
292
- logger.info({ duration: `${duration}ms` }, `Prompt result: ${promptName}`);
292
+ logger.info({ duration: `${duration}ms` }, `Prompt result: ${params.name}`);
293
293
  return result;
294
294
  }
295
- async complete(ref, argument) {
296
- const { mcpName, originalRef } = this.resolveCompletionRef(ref);
295
+ async complete(params) {
296
+ const { mcpName, originalRef } = this.resolveCompletionRef(params.ref);
297
297
  const mcp = this.mcps.get(mcpName);
298
298
  if (!mcp) {
299
299
  logger.error({ mcpName }, "MCP not found for completion");
300
300
  throw new Error(`MCP not found: ${mcpName}`);
301
301
  }
302
- logger.info({ ref, argument }, "Completion request");
302
+ logger.info({ ref: params.ref, argument: params.argument }, "Completion request");
303
303
  const startTime = Date.now();
304
- const result = await mcp.client.complete({ ref: originalRef, argument });
304
+ const result = await mcp.client.complete({
305
+ ref: originalRef,
306
+ argument: params.argument,
307
+ });
305
308
  const duration = Date.now() - startTime;
306
309
  logger.info({ duration: `${duration}ms` }, "Completion result");
307
310
  return result;
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { OAuthServer } from "./auth/oauth.js";
7
7
  import { GitHubIdentityProvider } from "./auth/providers/github.js";
8
8
  import { LocalIdentityProvider } from "./auth/providers/local.js";
9
9
  import { logger } from "./logger.js";
10
- import { handleCompletionComplete, handleInitialize, handleInitialized, handleMethodNotFound, handlePing, handlePromptsGet, handlePromptsList, handleResourcesList, handleResourcesRead, handleToolsCall, handleToolsList, } from "./mcp/handlers.js";
10
+ import { createMcpHandler } from "./mcp/handler.js";
11
11
  import { McpManager } from "./mcp/manager.js";
12
12
  import { MemoryStore } from "./storage/memory.js";
13
13
  import { SqliteStore } from "./storage/sqlite.js";
@@ -150,67 +150,15 @@ export async function createServer(config) {
150
150
  }
151
151
  return next();
152
152
  });
153
- // MCP handler
154
- const handleMcp = async (c) => {
155
- let message;
156
- try {
157
- message = await c.req.json();
158
- }
159
- catch (e) {
160
- logger.warn({ error: e instanceof Error ? e.message : String(e) }, "MCP parse error");
161
- return c.json({
162
- jsonrpc: "2.0",
163
- error: { code: -32700, message: "Parse error" },
164
- id: null,
165
- }, 400);
166
- }
167
- const method = "method" in message ? message.method : undefined;
168
- const id = "id" in message ? message.id : undefined;
169
- logger.debug({ method, id }, "MCP request");
170
- if (method === "initialize") {
171
- return handleInitialize(c, message);
172
- }
173
- if (method === "notifications/initialized") {
174
- return handleInitialized(c);
175
- }
176
- if (method === "tools/list") {
177
- return handleToolsList(c, message, mcpManager);
178
- }
179
- if (method === "tools/call") {
180
- const params = message.params;
181
- return handleToolsCall(c, message, mcpManager, params);
182
- }
183
- if (method === "resources/list") {
184
- return handleResourcesList(c, message, mcpManager);
185
- }
186
- if (method === "resources/read") {
187
- const params = message.params;
188
- return handleResourcesRead(c, message, mcpManager, params);
189
- }
190
- if (method === "prompts/list") {
191
- return handlePromptsList(c, message, mcpManager);
192
- }
193
- if (method === "prompts/get") {
194
- const params = message.params;
195
- return handlePromptsGet(c, message, mcpManager, params);
196
- }
197
- if (method === "ping") {
198
- return handlePing(c, message);
199
- }
200
- if (method === "completion/complete") {
201
- const params = message.params;
202
- return handleCompletionComplete(c, message, mcpManager, params);
203
- }
204
- return handleMethodNotFound(c, message, method ?? "unknown");
205
- };
206
153
  // Status endpoint (protected)
207
154
  protectedRoutes.get("/status", async (c) => {
208
155
  const health = await mcpManager.checkHealth();
209
156
  return c.json({ servers: health.servers });
210
157
  });
211
158
  // MCP endpoints (protected)
212
- protectedRoutes.post("/", handleMcp);
213
- protectedRoutes.post("/mcp", handleMcp);
159
+ const mcp = createMcpHandler(mcpManager);
160
+ protectedRoutes.post("/", mcp);
161
+ protectedRoutes.post("/mcp", mcp);
214
162
  // Mount protected routes
215
163
  app.route("/", protectedRoutes);
216
164
  // 404 for other routes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpbox",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A lightweight gateway that exposes local stdio-based MCP servers via Streamable HTTP",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -51,7 +51,7 @@
51
51
  "dependencies": {
52
52
  "@hono/node-server": "^1.19.9",
53
53
  "@modelcontextprotocol/sdk": "^1.25.3",
54
- "bcryptjs": "^2.4.3",
54
+ "bcryptjs": "^3.0.0",
55
55
  "hono": "^4.11.7",
56
56
  "pino": "^10.3.0",
57
57
  "pino-pretty": "^13.1.3",
@@ -59,7 +59,6 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@biomejs/biome": "^2.3.14",
62
- "@types/bcryptjs": "^2.4.6",
63
62
  "@types/node": "^25.1.0",
64
63
  "tsx": "^4.21.0",
65
64
  "typescript": "^5.9.3"
@@ -1,433 +0,0 @@
1
- import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
2
- import type { Context } from "hono";
3
- import type { McpManager } from "./manager.js";
4
- export declare function handleInitialize(c: Context, message: JSONRPCMessage): Response & import("hono").TypedResponse<{
5
- jsonrpc: string;
6
- id: string | number | undefined;
7
- result: {
8
- protocolVersion: string;
9
- capabilities: {
10
- tools: {
11
- listChanged: true;
12
- };
13
- resources: {
14
- listChanged: true;
15
- };
16
- prompts: {
17
- listChanged: true;
18
- };
19
- completions: {};
20
- };
21
- serverInfo: {
22
- name: string;
23
- version: string;
24
- };
25
- };
26
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">;
27
- export declare function handleInitialized(c: Context): Response & import("hono").TypedResponse<null, 202, "body">;
28
- export declare function handleToolsList(c: Context, message: JSONRPCMessage, mcpManager: McpManager): Response & import("hono").TypedResponse<{
29
- jsonrpc: string;
30
- id: string | number | undefined;
31
- result: {
32
- tools: {
33
- inputSchema: {
34
- [x: string]: import("hono/utils/types").JSONValue;
35
- type: "object";
36
- properties?: {
37
- [x: string]: never;
38
- } | undefined;
39
- required?: string[] | undefined;
40
- };
41
- name: string;
42
- description?: string | undefined;
43
- outputSchema?: {
44
- [x: string]: import("hono/utils/types").JSONValue;
45
- type: "object";
46
- properties?: {
47
- [x: string]: never;
48
- } | undefined;
49
- required?: string[] | undefined;
50
- } | undefined;
51
- annotations?: {
52
- title?: string | undefined;
53
- readOnlyHint?: boolean | undefined;
54
- destructiveHint?: boolean | undefined;
55
- idempotentHint?: boolean | undefined;
56
- openWorldHint?: boolean | undefined;
57
- } | undefined;
58
- execution?: {
59
- taskSupport?: "optional" | "required" | "forbidden" | undefined;
60
- } | undefined;
61
- _meta?: {
62
- [x: string]: import("hono/utils/types").JSONValue;
63
- } | undefined;
64
- icons?: {
65
- src: string;
66
- mimeType?: string | undefined;
67
- sizes?: string[] | undefined;
68
- theme?: "light" | "dark" | undefined;
69
- }[] | undefined;
70
- title?: string | undefined;
71
- }[];
72
- };
73
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">;
74
- export declare function handleToolsCall(c: Context, message: JSONRPCMessage, mcpManager: McpManager, params: {
75
- name: string;
76
- arguments?: Record<string, unknown>;
77
- }): Promise<(Response & import("hono").TypedResponse<{
78
- jsonrpc: string;
79
- id: string | number | undefined;
80
- result: {
81
- [x: string]: import("hono/utils/types").JSONValue;
82
- content: ({
83
- type: "text";
84
- text: string;
85
- annotations?: {
86
- audience?: ("user" | "assistant")[] | undefined;
87
- priority?: number | undefined;
88
- lastModified?: string | undefined;
89
- } | undefined;
90
- _meta?: {
91
- [x: string]: import("hono/utils/types").JSONValue;
92
- } | undefined;
93
- } | {
94
- type: "image";
95
- data: string;
96
- mimeType: string;
97
- annotations?: {
98
- audience?: ("user" | "assistant")[] | undefined;
99
- priority?: number | undefined;
100
- lastModified?: string | undefined;
101
- } | undefined;
102
- _meta?: {
103
- [x: string]: import("hono/utils/types").JSONValue;
104
- } | undefined;
105
- } | {
106
- type: "audio";
107
- data: string;
108
- mimeType: string;
109
- annotations?: {
110
- audience?: ("user" | "assistant")[] | undefined;
111
- priority?: number | undefined;
112
- lastModified?: string | undefined;
113
- } | undefined;
114
- _meta?: {
115
- [x: string]: import("hono/utils/types").JSONValue;
116
- } | undefined;
117
- } | {
118
- uri: string;
119
- name: string;
120
- type: "resource_link";
121
- description?: string | undefined;
122
- mimeType?: string | undefined;
123
- annotations?: {
124
- audience?: ("user" | "assistant")[] | undefined;
125
- priority?: number | undefined;
126
- lastModified?: string | undefined;
127
- } | undefined;
128
- _meta?: {
129
- [x: string]: import("hono/utils/types").JSONValue;
130
- } | undefined;
131
- icons?: {
132
- src: string;
133
- mimeType?: string | undefined;
134
- sizes?: string[] | undefined;
135
- theme?: "light" | "dark" | undefined;
136
- }[] | undefined;
137
- title?: string | undefined;
138
- } | {
139
- type: "resource";
140
- resource: {
141
- uri: string;
142
- text: string;
143
- mimeType?: string | undefined;
144
- _meta?: {
145
- [x: string]: import("hono/utils/types").JSONValue;
146
- } | undefined;
147
- } | {
148
- uri: string;
149
- blob: string;
150
- mimeType?: string | undefined;
151
- _meta?: {
152
- [x: string]: import("hono/utils/types").JSONValue;
153
- } | undefined;
154
- };
155
- annotations?: {
156
- audience?: ("user" | "assistant")[] | undefined;
157
- priority?: number | undefined;
158
- lastModified?: string | undefined;
159
- } | undefined;
160
- _meta?: {
161
- [x: string]: import("hono/utils/types").JSONValue;
162
- } | undefined;
163
- })[];
164
- _meta?: {
165
- [x: string]: import("hono/utils/types").JSONValue;
166
- progressToken?: string | number | undefined;
167
- "io.modelcontextprotocol/related-task"?: {
168
- taskId: string;
169
- } | undefined;
170
- } | undefined;
171
- structuredContent?: {
172
- [x: string]: import("hono/utils/types").JSONValue;
173
- } | undefined;
174
- isError?: boolean | undefined;
175
- };
176
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
177
- jsonrpc: string;
178
- id: string | number | undefined;
179
- error: {
180
- code: number;
181
- message: string;
182
- };
183
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
184
- export declare function handlePing(c: Context, message: JSONRPCMessage): Response & import("hono").TypedResponse<{
185
- jsonrpc: string;
186
- id: string | number | undefined;
187
- result: {};
188
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">;
189
- export declare function handleResourcesList(c: Context, message: JSONRPCMessage, mcpManager: McpManager): Response & import("hono").TypedResponse<{
190
- jsonrpc: string;
191
- id: string | number | undefined;
192
- result: {
193
- resources: {
194
- uri: string;
195
- name: string;
196
- description?: string | undefined;
197
- mimeType?: string | undefined;
198
- annotations?: {
199
- audience?: ("user" | "assistant")[] | undefined;
200
- priority?: number | undefined;
201
- lastModified?: string | undefined;
202
- } | undefined;
203
- _meta?: {
204
- [x: string]: import("hono/utils/types").JSONValue;
205
- } | undefined;
206
- icons?: {
207
- src: string;
208
- mimeType?: string | undefined;
209
- sizes?: string[] | undefined;
210
- theme?: "light" | "dark" | undefined;
211
- }[] | undefined;
212
- title?: string | undefined;
213
- }[];
214
- };
215
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">;
216
- export declare function handleResourcesRead(c: Context, message: JSONRPCMessage, mcpManager: McpManager, params: {
217
- uri: string;
218
- }): Promise<(Response & import("hono").TypedResponse<{
219
- jsonrpc: string;
220
- id: string | number | undefined;
221
- result: {
222
- [x: string]: import("hono/utils/types").JSONValue;
223
- contents: ({
224
- uri: string;
225
- text: string;
226
- mimeType?: string | undefined;
227
- _meta?: {
228
- [x: string]: import("hono/utils/types").JSONValue;
229
- } | undefined;
230
- } | {
231
- uri: string;
232
- blob: string;
233
- mimeType?: string | undefined;
234
- _meta?: {
235
- [x: string]: import("hono/utils/types").JSONValue;
236
- } | undefined;
237
- })[];
238
- _meta?: {
239
- [x: string]: import("hono/utils/types").JSONValue;
240
- progressToken?: string | number | undefined;
241
- "io.modelcontextprotocol/related-task"?: {
242
- taskId: string;
243
- } | undefined;
244
- } | undefined;
245
- };
246
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
247
- jsonrpc: string;
248
- id: string | number | undefined;
249
- error: {
250
- code: number;
251
- message: string;
252
- };
253
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
254
- export declare function handlePromptsList(c: Context, message: JSONRPCMessage, mcpManager: McpManager): Response & import("hono").TypedResponse<{
255
- jsonrpc: string;
256
- id: string | number | undefined;
257
- result: {
258
- prompts: {
259
- name: string;
260
- description?: string | undefined;
261
- arguments?: {
262
- name: string;
263
- description?: string | undefined;
264
- required?: boolean | undefined;
265
- }[] | undefined;
266
- _meta?: {
267
- [x: string]: import("hono/utils/types").JSONValue;
268
- } | undefined;
269
- icons?: {
270
- src: string;
271
- mimeType?: string | undefined;
272
- sizes?: string[] | undefined;
273
- theme?: "light" | "dark" | undefined;
274
- }[] | undefined;
275
- title?: string | undefined;
276
- }[];
277
- };
278
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">;
279
- export declare function handlePromptsGet(c: Context, message: JSONRPCMessage, mcpManager: McpManager, params: {
280
- name: string;
281
- arguments?: Record<string, string>;
282
- }): Promise<(Response & import("hono").TypedResponse<{
283
- jsonrpc: string;
284
- id: string | number | undefined;
285
- result: {
286
- [x: string]: import("hono/utils/types").JSONValue;
287
- messages: {
288
- role: "user" | "assistant";
289
- content: {
290
- type: "text";
291
- text: string;
292
- annotations?: {
293
- audience?: ("user" | "assistant")[] | undefined;
294
- priority?: number | undefined;
295
- lastModified?: string | undefined;
296
- } | undefined;
297
- _meta?: {
298
- [x: string]: import("hono/utils/types").JSONValue;
299
- } | undefined;
300
- } | {
301
- type: "image";
302
- data: string;
303
- mimeType: string;
304
- annotations?: {
305
- audience?: ("user" | "assistant")[] | undefined;
306
- priority?: number | undefined;
307
- lastModified?: string | undefined;
308
- } | undefined;
309
- _meta?: {
310
- [x: string]: import("hono/utils/types").JSONValue;
311
- } | undefined;
312
- } | {
313
- type: "audio";
314
- data: string;
315
- mimeType: string;
316
- annotations?: {
317
- audience?: ("user" | "assistant")[] | undefined;
318
- priority?: number | undefined;
319
- lastModified?: string | undefined;
320
- } | undefined;
321
- _meta?: {
322
- [x: string]: import("hono/utils/types").JSONValue;
323
- } | undefined;
324
- } | {
325
- uri: string;
326
- name: string;
327
- type: "resource_link";
328
- description?: string | undefined;
329
- mimeType?: string | undefined;
330
- annotations?: {
331
- audience?: ("user" | "assistant")[] | undefined;
332
- priority?: number | undefined;
333
- lastModified?: string | undefined;
334
- } | undefined;
335
- _meta?: {
336
- [x: string]: import("hono/utils/types").JSONValue;
337
- } | undefined;
338
- icons?: {
339
- src: string;
340
- mimeType?: string | undefined;
341
- sizes?: string[] | undefined;
342
- theme?: "light" | "dark" | undefined;
343
- }[] | undefined;
344
- title?: string | undefined;
345
- } | {
346
- type: "resource";
347
- resource: {
348
- uri: string;
349
- text: string;
350
- mimeType?: string | undefined;
351
- _meta?: {
352
- [x: string]: import("hono/utils/types").JSONValue;
353
- } | undefined;
354
- } | {
355
- uri: string;
356
- blob: string;
357
- mimeType?: string | undefined;
358
- _meta?: {
359
- [x: string]: import("hono/utils/types").JSONValue;
360
- } | undefined;
361
- };
362
- annotations?: {
363
- audience?: ("user" | "assistant")[] | undefined;
364
- priority?: number | undefined;
365
- lastModified?: string | undefined;
366
- } | undefined;
367
- _meta?: {
368
- [x: string]: import("hono/utils/types").JSONValue;
369
- } | undefined;
370
- };
371
- }[];
372
- _meta?: {
373
- [x: string]: import("hono/utils/types").JSONValue;
374
- progressToken?: string | number | undefined;
375
- "io.modelcontextprotocol/related-task"?: {
376
- taskId: string;
377
- } | undefined;
378
- } | undefined;
379
- description?: string | undefined;
380
- };
381
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
382
- jsonrpc: string;
383
- id: string | number | undefined;
384
- error: {
385
- code: number;
386
- message: string;
387
- };
388
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
389
- export declare function handleCompletionComplete(c: Context, message: JSONRPCMessage, mcpManager: McpManager, params: {
390
- ref: {
391
- type: string;
392
- name?: string;
393
- uri?: string;
394
- };
395
- argument: {
396
- name: string;
397
- value: string;
398
- };
399
- }): Promise<(Response & import("hono").TypedResponse<{
400
- jsonrpc: string;
401
- id: string | number | undefined;
402
- result: {
403
- [x: string]: import("hono/utils/types").JSONValue;
404
- completion: {
405
- [x: string]: import("hono/utils/types").JSONValue;
406
- values: string[];
407
- total?: number | undefined;
408
- hasMore?: boolean | undefined;
409
- };
410
- _meta?: {
411
- [x: string]: import("hono/utils/types").JSONValue;
412
- progressToken?: string | number | undefined;
413
- "io.modelcontextprotocol/related-task"?: {
414
- taskId: string;
415
- } | undefined;
416
- } | undefined;
417
- };
418
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
419
- jsonrpc: string;
420
- id: string | number | undefined;
421
- error: {
422
- code: number;
423
- message: string;
424
- };
425
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">)>;
426
- export declare function handleMethodNotFound(c: Context, message: JSONRPCMessage, method: string): Response & import("hono").TypedResponse<{
427
- jsonrpc: string;
428
- id: string | number | null;
429
- error: {
430
- code: number;
431
- message: string;
432
- };
433
- }, import("hono/utils/http-status").ContentfulStatusCode, "json">;
@@ -1,144 +0,0 @@
1
- import { NAME, VERSION } from "../version.js";
2
- function getMessageId(message) {
3
- return "id" in message ? message.id : undefined;
4
- }
5
- export function handleInitialize(c, message) {
6
- return c.json({
7
- jsonrpc: "2.0",
8
- id: getMessageId(message),
9
- result: {
10
- protocolVersion: "2025-11-25",
11
- capabilities: {
12
- tools: { listChanged: true },
13
- resources: { listChanged: true },
14
- prompts: { listChanged: true },
15
- completions: {},
16
- },
17
- serverInfo: {
18
- name: NAME,
19
- version: VERSION,
20
- },
21
- },
22
- });
23
- }
24
- export function handleInitialized(c) {
25
- return c.body(null, 202);
26
- }
27
- export function handleToolsList(c, message, mcpManager) {
28
- const tools = mcpManager.listTools();
29
- return c.json({
30
- jsonrpc: "2.0",
31
- id: getMessageId(message),
32
- result: { tools },
33
- });
34
- }
35
- export async function handleToolsCall(c, message, mcpManager, params) {
36
- try {
37
- const result = await mcpManager.callTool(params.name, params.arguments ?? {});
38
- return c.json({
39
- jsonrpc: "2.0",
40
- id: getMessageId(message),
41
- result,
42
- });
43
- }
44
- catch (error) {
45
- return c.json({
46
- jsonrpc: "2.0",
47
- id: getMessageId(message),
48
- error: {
49
- code: -32603,
50
- message: error instanceof Error ? error.message : "Internal error",
51
- },
52
- });
53
- }
54
- }
55
- export function handlePing(c, message) {
56
- return c.json({
57
- jsonrpc: "2.0",
58
- id: getMessageId(message),
59
- result: {},
60
- });
61
- }
62
- export function handleResourcesList(c, message, mcpManager) {
63
- const resources = mcpManager.listResources();
64
- return c.json({
65
- jsonrpc: "2.0",
66
- id: getMessageId(message),
67
- result: { resources },
68
- });
69
- }
70
- export async function handleResourcesRead(c, message, mcpManager, params) {
71
- try {
72
- const result = await mcpManager.readResource(params.uri);
73
- return c.json({
74
- jsonrpc: "2.0",
75
- id: getMessageId(message),
76
- result,
77
- });
78
- }
79
- catch (error) {
80
- return c.json({
81
- jsonrpc: "2.0",
82
- id: getMessageId(message),
83
- error: {
84
- code: -32603,
85
- message: error instanceof Error ? error.message : "Internal error",
86
- },
87
- });
88
- }
89
- }
90
- export function handlePromptsList(c, message, mcpManager) {
91
- const prompts = mcpManager.listPrompts();
92
- return c.json({
93
- jsonrpc: "2.0",
94
- id: getMessageId(message),
95
- result: { prompts },
96
- });
97
- }
98
- export async function handlePromptsGet(c, message, mcpManager, params) {
99
- try {
100
- const result = await mcpManager.getPrompt(params.name, params.arguments);
101
- return c.json({
102
- jsonrpc: "2.0",
103
- id: getMessageId(message),
104
- result,
105
- });
106
- }
107
- catch (error) {
108
- return c.json({
109
- jsonrpc: "2.0",
110
- id: getMessageId(message),
111
- error: {
112
- code: -32603,
113
- message: error instanceof Error ? error.message : "Internal error",
114
- },
115
- });
116
- }
117
- }
118
- export async function handleCompletionComplete(c, message, mcpManager, params) {
119
- try {
120
- const result = await mcpManager.complete(params.ref, params.argument);
121
- return c.json({
122
- jsonrpc: "2.0",
123
- id: getMessageId(message),
124
- result,
125
- });
126
- }
127
- catch (error) {
128
- return c.json({
129
- jsonrpc: "2.0",
130
- id: getMessageId(message),
131
- error: {
132
- code: -32603,
133
- message: error instanceof Error ? error.message : "Internal error",
134
- },
135
- });
136
- }
137
- }
138
- export function handleMethodNotFound(c, message, method) {
139
- return c.json({
140
- jsonrpc: "2.0",
141
- id: getMessageId(message) ?? null,
142
- error: { code: -32601, message: `Method not found: ${method}` },
143
- });
144
- }