mcpbox 0.2.2 → 0.3.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
@@ -3,6 +3,8 @@
3
3
  <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
4
  <img src="assets/logo.svg" width="128" alt="MCPBox">
5
5
  </picture>
6
+ <br>
7
+ <a href="https://www.npmjs.com/package/mcpbox"><img src="https://img.shields.io/npm/v/mcpbox?style=flat-square" alt="npm version"></a>
6
8
  </p>
7
9
 
8
10
  **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.
@@ -11,7 +13,7 @@
11
13
  - Supports Tools, Resources & Prompts
12
14
  - Namespaces with `servername__` prefix to avoid collisions
13
15
  - Per-server tool filtering to limit AI access and reduce context usage
14
- - OAuth or API key authentication
16
+ - OAuth 2.1, API key, or no auth
15
17
 
16
18
  <picture>
17
19
  <source media="(prefers-color-scheme: dark)" srcset="assets/diagram-dark.excalidraw.png">
@@ -28,137 +30,33 @@ Create `mcpbox.json`:
28
30
  "memory": {
29
31
  "command": "npx",
30
32
  "args": ["-y", "@modelcontextprotocol/server-memory"]
33
+ },
34
+ "sequential-thinking": {
35
+ "command": "npx",
36
+ "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
31
37
  }
32
38
  }
33
39
  }
34
40
  ```
35
41
 
36
- Run with:
37
-
38
- **npx**
42
+ Run:
39
43
 
40
44
  ```bash
41
45
  npx mcpbox
42
46
  ```
43
- *MCP server commands (e.g., `uvx`, `docker`) must be available where the box runs.*
44
-
45
- **Docker**
46
-
47
- ```bash
48
- docker run -v ./mcpbox.json:/config/config.json -p 8080:8080 ghcr.io/kandobyte/mcpbox
49
- ```
50
- *The Docker image includes Node.js and Python, supporting MCP servers launched via `npx` and `uvx`.*
51
47
 
52
- The box starts on http://localhost:8080. Connect an agent by adding this to your MCP client config:
48
+ Add to your MCP client config:
53
49
 
54
50
  ```json
55
51
  {
56
52
  "mcpServers": {
57
53
  "mcpbox": {
58
- "type": "http",
59
54
  "url": "http://localhost:8080"
60
55
  }
61
56
  }
62
57
  }
63
58
  ```
64
59
 
65
- For remote access with authentication, see [Deployment](#deployment) and [Connect Your AI](#connect-your-ai).
66
-
67
- ## Configuration
68
-
69
- See [`mcpbox.example.jsonc`](mcpbox.example.jsonc) for all options. All string values support `${VAR_NAME}` environment variable substitution.
70
-
71
- **[Authentication](docs/authentication.md)** — none (default), API key, or OAuth.
72
-
73
- ## Deployment
74
-
75
- To expose MCPBox remotely, put it behind a TLS-terminating reverse proxy.
76
-
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
81
-
82
- > [!NOTE]
83
- > MCPBox is single-instance only — don't run multiple instances behind a load balancer.
84
-
85
- ### Quick remote access
86
-
87
- Use [cloudflared](https://github.com/cloudflare/cloudflared) to expose a local instance (no account required):
88
-
89
- ```bash
90
- cloudflared tunnel --url http://localhost:8080
91
- ```
92
-
93
- Then update your config with the generated public URL:
94
-
95
- ```json
96
- {
97
- "auth": {
98
- "type": "oauth",
99
- "issuer": "https://<tunnel-id>.trycloudflare.com",
100
- "identityProviders": [
101
- { "type": "local", "users": [{ "username": "admin", "password": "${MCPBOX_PASSWORD}" }] }
102
- ],
103
- "dynamicRegistration": true
104
- },
105
- "storage": {
106
- "type": "sqlite",
107
- "path": "/data/mcpbox.db"
108
- },
109
- "mcpServers": { ... }
110
- }
111
- ```
112
-
113
- Run with a persistent data volume:
114
-
115
- ```bash
116
- docker run -v ./mcpbox.json:/config/config.json -v ./data:/data -p 8080:8080 ghcr.io/kandobyte/mcpbox
117
- ```
118
-
119
- ## Connect Your AI
60
+ ## Documentation
120
61
 
121
- ### Claude Web & Mobile
122
-
123
- Settings → Connectors → Add Custom Connector → enter your URL → Connect
124
-
125
- Requires `dynamicRegistration: true` in your config.
126
-
127
- ### Claude Code
128
-
129
- ```bash
130
- claude mcp add --transport http mcpbox https://your-mcpbox-url.com
131
- ```
132
-
133
- Requires `dynamicRegistration: true` in your config.
134
-
135
- ### Other MCP clients
136
-
137
- **With dynamic registration (OAuth)** — just provide the URL:
138
-
139
- ```json
140
- {
141
- "mcpServers": {
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",
157
- "url": "https://your-mcpbox-url.com",
158
- "headers": {
159
- "Authorization": "Bearer YOUR_API_KEY"
160
- }
161
- }
162
- }
163
- }
164
- ```
62
+ See the [documentation](https://kandobyte.github.io/mcpbox/quick-start) for configuration, authentication, deployment, and connecting clients.
@@ -2,7 +2,7 @@ import type { Context } from "hono";
2
2
  import type { OAuthClient } from "../config/types.js";
3
3
  import type { StateStore } from "../storage/types.js";
4
4
  import type { IdentityProvider } from "./providers/identity-provider.js";
5
- export interface OAuthConfig {
5
+ interface OAuthConfig {
6
6
  issuer: string;
7
7
  providers: IdentityProvider[];
8
8
  clients?: OAuthClient[];
@@ -35,3 +35,4 @@ export declare class OAuthServer {
35
35
  };
36
36
  sendUnauthorized(c: Context, error?: string): Response;
37
37
  }
38
+ export {};
@@ -406,7 +406,10 @@ export class OAuthServer {
406
406
  box-sizing: border-box;
407
407
  }
408
408
  body {
409
- font-family: system-ui, -apple-system, sans-serif;
409
+ font-family:
410
+ system-ui,
411
+ -apple-system,
412
+ sans-serif;
410
413
  background: #fafaf9;
411
414
  margin: 0;
412
415
  padding: 20px;
@@ -426,8 +429,8 @@ export class OAuthServer {
426
429
  max-width: 380px;
427
430
  }
428
431
  h1 {
429
- font-family: ui-monospace, "SF Mono", Menlo, Monaco, Consolas,
430
- monospace;
432
+ font-family:
433
+ ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
431
434
  font-size: 28px;
432
435
  font-weight: 600;
433
436
  color: #1c1917;
@@ -562,11 +565,7 @@ export class OAuthServer {
562
565
  ${hasForm && hasRedirect ? html `<div class="divider">or</div>` : ""}
563
566
  ${hasForm
564
567
  ? html `<form method="POST" action="${formAction}">
565
- <input
566
- type="hidden"
567
- name="session_id"
568
- value="${sessionId}"
569
- />
568
+ <input type="hidden" name="session_id" value="${sessionId}" />
570
569
  <div class="field">
571
570
  <label for="username">Username</label>
572
571
  <input
@@ -2,7 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { RawConfigSchema, } from "./schema.js";
3
3
  function substituteEnvVars(obj) {
4
4
  if (typeof obj === "string") {
5
- return obj.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "");
5
+ return obj.replace(/\$\{(\w+)\}/g, (match, name) => {
6
+ const value = process.env[name];
7
+ if (value === undefined) {
8
+ throw new Error(`Environment variable ${name} is not set (referenced as ${match})`);
9
+ }
10
+ return value;
11
+ });
6
12
  }
7
13
  if (Array.isArray(obj)) {
8
14
  return obj.map(substituteEnvVars);
@@ -25,6 +31,8 @@ function parseMcpServers(mcpServers) {
25
31
  args: entry.args,
26
32
  env: entry.env,
27
33
  tools: entry.tools,
34
+ resources: entry.resources,
35
+ prompts: entry.prompts,
28
36
  });
29
37
  }
30
38
  return mcps;
@@ -8,6 +8,8 @@ export declare const McpServerEntrySchema: z.ZodObject<{
8
8
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
9
9
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
10
10
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ resources: z.ZodOptional<z.ZodBoolean>;
12
+ prompts: z.ZodOptional<z.ZodBoolean>;
11
13
  }, z.core.$strict>;
12
14
  /**
13
15
  * OAuth user credentials
@@ -17,28 +19,6 @@ export declare const OAuthUserSchema: z.ZodObject<{
17
19
  username: z.ZodString;
18
20
  password: z.ZodString;
19
21
  }, z.core.$strict>;
20
- /**
21
- * Local identity provider — users defined in config.
22
- * @package
23
- */
24
- export declare const LocalIdPSchema: z.ZodObject<{
25
- type: z.ZodLiteral<"local">;
26
- users: z.ZodArray<z.ZodObject<{
27
- username: z.ZodString;
28
- password: z.ZodString;
29
- }, z.core.$strict>>;
30
- }, z.core.$strict>;
31
- /**
32
- * GitHub identity provider — OAuth web flow.
33
- * @package
34
- */
35
- export declare const GitHubIdPSchema: z.ZodObject<{
36
- type: z.ZodLiteral<"github">;
37
- clientId: z.ZodString;
38
- clientSecret: z.ZodString;
39
- allowedOrgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
- allowedUsers: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
- }, z.core.$strict>;
42
22
  /**
43
23
  * Identity provider configuration — discriminated union.
44
24
  * @package
@@ -204,6 +184,8 @@ export declare const RawConfigSchema: z.ZodObject<{
204
184
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
205
185
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
206
186
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
187
+ resources: z.ZodOptional<z.ZodBoolean>;
188
+ prompts: z.ZodOptional<z.ZodBoolean>;
207
189
  }, z.core.$strict>>>;
208
190
  }, z.core.$strict>;
209
191
  /**
@@ -216,6 +198,8 @@ export declare const McpConfigSchema: z.ZodObject<{
216
198
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
217
199
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
218
200
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
201
+ resources: z.ZodOptional<z.ZodBoolean>;
202
+ prompts: z.ZodOptional<z.ZodBoolean>;
219
203
  }, z.core.$strip>;
220
204
  /**
221
205
  * Processed config (after loader adds defaults and resolves mcpServers)
@@ -282,19 +266,13 @@ export declare const ConfigSchema: z.ZodObject<{
282
266
  args: z.ZodOptional<z.ZodArray<z.ZodString>>;
283
267
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
284
268
  tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
269
+ resources: z.ZodOptional<z.ZodBoolean>;
270
+ prompts: z.ZodOptional<z.ZodBoolean>;
285
271
  }, z.core.$strip>>;
286
272
  }, z.core.$strip>;
287
- export type McpServerEntry = z.infer<typeof McpServerEntrySchema>;
288
- export type OAuthUser = z.infer<typeof OAuthUserSchema>;
289
273
  export type OAuthClient = z.infer<typeof OAuthClientSchema>;
290
- export type IdentityProviderConfig = z.infer<typeof IdentityProviderSchema>;
291
- export type LocalIdPConfig = z.infer<typeof LocalIdPSchema>;
292
- export type GitHubIdPConfig = z.infer<typeof GitHubIdPSchema>;
293
- export type AuthConfig = z.infer<typeof AuthConfigSchema>;
294
- export type ServerConfig = z.infer<typeof ServerConfigSchema>;
295
274
  export type LogConfig = z.infer<typeof LogConfigSchema>;
296
- export type StorageConfig = z.infer<typeof StorageConfigSchema>;
297
- export type RawConfig = z.infer<typeof RawConfigSchema>;
275
+ export type McpServerEntry = z.infer<typeof McpServerEntrySchema>;
298
276
  export type McpConfig = z.infer<typeof McpConfigSchema>;
299
277
  export type Config = z.infer<typeof ConfigSchema>;
300
278
  export interface LoadConfigResult {
@@ -17,6 +17,8 @@ export const McpServerEntrySchema = z
17
17
  args: z.array(z.string()).optional(),
18
18
  env: z.record(z.string(), z.string()).optional(),
19
19
  tools: z.array(z.string()).optional(),
20
+ resources: z.boolean().optional(),
21
+ prompts: z.boolean().optional(),
20
22
  })
21
23
  .strict();
22
24
  /**
@@ -33,7 +35,7 @@ export const OAuthUserSchema = z
33
35
  * Local identity provider — users defined in config.
34
36
  * @package
35
37
  */
36
- export const LocalIdPSchema = z
38
+ const LocalIdPSchema = z
37
39
  .object({
38
40
  type: z.literal("local"),
39
41
  users: z.array(OAuthUserSchema).min(1, "At least one user is required"),
@@ -43,7 +45,7 @@ export const LocalIdPSchema = z
43
45
  * GitHub identity provider — OAuth web flow.
44
46
  * @package
45
47
  */
46
- export const GitHubIdPSchema = z
48
+ const GitHubIdPSchema = z
47
49
  .object({
48
50
  type: z.literal("github"),
49
51
  clientId: z.string().min(1, "GitHub clientId is required"),
@@ -201,6 +203,8 @@ export const McpConfigSchema = z.object({
201
203
  args: z.array(z.string()).optional(),
202
204
  env: z.record(z.string(), z.string()).optional(),
203
205
  tools: z.array(z.string()).optional(),
206
+ resources: z.boolean().optional(),
207
+ prompts: z.boolean().optional(),
204
208
  });
205
209
  /**
206
210
  * Processed config (after loader adds defaults and resolves mcpServers)
@@ -1,5 +1 @@
1
- export type { AuthConfig, Config, GitHubIdPConfig, IdentityProviderConfig, LocalIdPConfig, LogConfig, McpConfig, McpServerEntry, OAuthClient, OAuthUser, ServerConfig, StorageConfig, } from "./schema.js";
2
- export type GrantType = "authorization_code" | "client_credentials";
3
- export type McpServersConfig = {
4
- mcpServers: Record<string, import("./schema.js").McpServerEntry>;
5
- };
1
+ export type { Config, LogConfig, McpConfig, OAuthClient } from "./schema.js";
@@ -1,15 +1,5 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
1
  import type { CallToolRequestParams, CallToolResult, CompleteRequestParams, CompleteResult, GetPromptRequestParams, GetPromptResult, Prompt, ReadResourceRequestParams, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
4
2
  import type { LogConfig, McpConfig } from "../config/types.js";
5
- export interface ManagedMcp {
6
- name: string;
7
- client: Client;
8
- transport: StdioClientTransport;
9
- tools: Tool[];
10
- resources: Resource[];
11
- prompts: Prompt[];
12
- }
13
3
  export declare class McpManager {
14
4
  private mcps;
15
5
  private toolToMcp;
@@ -106,37 +106,47 @@ export class McpManager {
106
106
  }
107
107
  // Get resources from this MCP (namespace them if multiple servers)
108
108
  let resources = [];
109
- try {
110
- const { resources: rawResources } = await client.listResources();
111
- resources = this.useNamespacing
112
- ? rawResources.map((resource) => ({
113
- ...resource,
114
- uri: namespaceName(config.name, resource.uri),
115
- }))
116
- : rawResources;
117
- for (const resource of resources) {
118
- this.resourceToMcp.set(resource.uri, config.name);
109
+ if (config.resources !== false) {
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.debug({ mcp: config.name }, "Server doesn't support resources");
119
124
  }
120
125
  }
121
- catch {
122
- logger.debug({ mcp: config.name }, "Server doesn't support resources");
126
+ else {
127
+ logger.debug({ mcp: config.name }, "Resources disabled by config");
123
128
  }
124
129
  // Get prompts from this MCP (namespace them if multiple servers)
125
130
  let prompts = [];
126
- try {
127
- const { prompts: rawPrompts } = await client.listPrompts();
128
- prompts = this.useNamespacing
129
- ? rawPrompts.map((prompt) => ({
130
- ...prompt,
131
- name: namespaceName(config.name, prompt.name),
132
- }))
133
- : rawPrompts;
134
- for (const prompt of prompts) {
135
- this.promptToMcp.set(prompt.name, config.name);
131
+ if (config.prompts !== false) {
132
+ try {
133
+ const { prompts: rawPrompts } = await client.listPrompts();
134
+ prompts = this.useNamespacing
135
+ ? rawPrompts.map((prompt) => ({
136
+ ...prompt,
137
+ name: namespaceName(config.name, prompt.name),
138
+ }))
139
+ : rawPrompts;
140
+ for (const prompt of prompts) {
141
+ this.promptToMcp.set(prompt.name, config.name);
142
+ }
143
+ }
144
+ catch {
145
+ logger.debug({ mcp: config.name }, "Server doesn't support prompts");
136
146
  }
137
147
  }
138
- catch {
139
- logger.debug({ mcp: config.name }, "Server doesn't support prompts");
148
+ else {
149
+ logger.debug({ mcp: config.name }, "Prompts disabled by config");
140
150
  }
141
151
  this.mcps.set(config.name, {
142
152
  name: config.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpbox",
3
- "version": "0.2.2",
3
+ "version": "0.3.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": {
@@ -17,8 +17,13 @@
17
17
  "test:conformance:mcp": "node --disable-warning=ExperimentalWarning --import tsx test/conformance-mcp/run.ts",
18
18
  "test:conformance:oauth": "node --disable-warning=ExperimentalWarning --import tsx --test --test-concurrency=1 'test/conformance-oauth/**/*.test.ts'",
19
19
  "test:coverage": "node --experimental-test-coverage --disable-warning=ExperimentalWarning --import tsx --test --test-concurrency=1 'test/unit/**/*.test.ts' 'test/integration/**/*.test.ts' 'test/conformance-oauth/**/*.test.ts'",
20
+ "test:mutation": "stryker run",
20
21
  "format": "biome check --write .",
21
- "check": "biome check ."
22
+ "check": "biome check .",
23
+ "knip": "knip",
24
+ "docs:dev": "vitepress dev docs",
25
+ "docs:build": "vitepress build docs",
26
+ "docs:preview": "vitepress preview docs"
22
27
  },
23
28
  "keywords": [
24
29
  "mcp",
@@ -50,17 +55,23 @@
50
55
  "type": "module",
51
56
  "dependencies": {
52
57
  "@hono/node-server": "^1.19.9",
53
- "@modelcontextprotocol/sdk": "^1.25.3",
58
+ "@modelcontextprotocol/sdk": "^1.27.1",
54
59
  "bcryptjs": "^3.0.0",
55
- "hono": "^4.11.7",
60
+ "hono": "^4.12.7",
56
61
  "pino": "^10.3.0",
57
62
  "pino-pretty": "^13.1.3",
58
63
  "zod": "^4.3.6"
59
64
  },
60
65
  "devDependencies": {
61
- "@biomejs/biome": "^2.3.14",
66
+ "@biomejs/biome": "^2.4.7",
67
+ "@stryker-mutator/core": "^9.6.0",
68
+ "@stryker-mutator/tap-runner": "^9.6.0",
69
+ "@stryker-mutator/typescript-checker": "^9.6.0",
62
70
  "@types/node": "^25.1.0",
71
+ "knip": "^5.86.0",
63
72
  "tsx": "^4.21.0",
64
- "typescript": "^5.9.3"
73
+ "typescript": "^5.9.3",
74
+ "vitepress": "^1.6.4",
75
+ "vitepress-plugin-llms": "^1.11.0"
65
76
  }
66
77
  }