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.
- package/LICENSE +190 -0
- package/README.md +157 -0
- package/dist/assets.d.ts +1 -0
- package/dist/assets.js +1 -0
- package/dist/auth/apikey.d.ts +4 -0
- package/dist/auth/apikey.js +9 -0
- package/dist/auth/crypto.d.ts +4 -0
- package/dist/auth/crypto.js +19 -0
- package/dist/auth/oauth-utils.d.ts +30 -0
- package/dist/auth/oauth-utils.js +51 -0
- package/dist/auth/oauth.d.ts +36 -0
- package/dist/auth/oauth.js +715 -0
- package/dist/config/loader.d.ts +3 -0
- package/dist/config/loader.js +134 -0
- package/dist/config/schema.d.ts +220 -0
- package/dist/config/schema.js +159 -0
- package/dist/config/types.d.ts +5 -0
- package/dist/config/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +117 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +65 -0
- package/dist/mcp/handlers.d.ts +433 -0
- package/dist/mcp/handlers.js +144 -0
- package/dist/mcp/manager.d.ts +48 -0
- package/dist/mcp/manager.js +341 -0
- package/dist/mcp/namespace.d.ts +29 -0
- package/dist/mcp/namespace.js +39 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +246 -0
- package/dist/storage/memory.d.ts +20 -0
- package/dist/storage/memory.js +87 -0
- package/dist/storage/sqlite.d.ts +22 -0
- package/dist/storage/sqlite.js +128 -0
- package/dist/storage/types.d.ts +46 -0
- package/dist/storage/types.js +2 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/package.json +64 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { RawConfigSchema, } from "./schema.js";
|
|
3
|
+
function substituteEnvVars(obj) {
|
|
4
|
+
if (typeof obj === "string") {
|
|
5
|
+
return obj.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "");
|
|
6
|
+
}
|
|
7
|
+
if (Array.isArray(obj)) {
|
|
8
|
+
return obj.map(substituteEnvVars);
|
|
9
|
+
}
|
|
10
|
+
if (obj && typeof obj === "object") {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
13
|
+
result[key] = substituteEnvVars(value);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
return obj;
|
|
18
|
+
}
|
|
19
|
+
function parseMcpServers(mcpServers) {
|
|
20
|
+
const mcps = [];
|
|
21
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
22
|
+
mcps.push({
|
|
23
|
+
name,
|
|
24
|
+
command: entry.command,
|
|
25
|
+
args: entry.args,
|
|
26
|
+
env: entry.env,
|
|
27
|
+
tools: entry.tools,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return mcps;
|
|
31
|
+
}
|
|
32
|
+
function formatZodIssue(issue) {
|
|
33
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
34
|
+
const code = issue.code;
|
|
35
|
+
// Handle discriminated union errors (wrong type value)
|
|
36
|
+
if (code === "invalid_union" && "discriminator" in issue) {
|
|
37
|
+
const discriminator = issue.discriminator;
|
|
38
|
+
// Get valid options from the schema based on the discriminator
|
|
39
|
+
if (discriminator === "type" && path === "auth.type") {
|
|
40
|
+
return `${path}: must be one of: "none", "apikey", "oauth"`;
|
|
41
|
+
}
|
|
42
|
+
if (discriminator === "type" && path === "storage.type") {
|
|
43
|
+
return `${path}: must be one of: "memory", "sqlite"`;
|
|
44
|
+
}
|
|
45
|
+
return `${path}: invalid value for "${discriminator}"`;
|
|
46
|
+
}
|
|
47
|
+
// Handle unrecognized keys
|
|
48
|
+
if (code === "unrecognized_keys" && "keys" in issue) {
|
|
49
|
+
const keys = issue.keys.map((k) => `"${k}"`).join(", ");
|
|
50
|
+
const location = path === "" ? "config" : path;
|
|
51
|
+
return `${location}: unknown field${issue.keys.length > 1 ? "s" : ""} ${keys}`;
|
|
52
|
+
}
|
|
53
|
+
// Handle missing required fields
|
|
54
|
+
if (code === "invalid_type" && "expected" in issue) {
|
|
55
|
+
const expected = issue.expected;
|
|
56
|
+
if (issue.message.includes("received undefined")) {
|
|
57
|
+
return `${path}: required field missing`;
|
|
58
|
+
}
|
|
59
|
+
return `${path}: expected ${expected}`;
|
|
60
|
+
}
|
|
61
|
+
// Handle enum errors (Zod v4 uses "invalid_value" with options)
|
|
62
|
+
if ("options" in issue && Array.isArray(issue.options)) {
|
|
63
|
+
const options = issue.options.map((o) => `"${o}"`).join(", ");
|
|
64
|
+
return `${path}: must be one of: ${options}`;
|
|
65
|
+
}
|
|
66
|
+
// Clean up Zod's default messages
|
|
67
|
+
const message = issue.message;
|
|
68
|
+
// "Invalid option: expected one of X" -> cleaner format
|
|
69
|
+
if (message.startsWith("Invalid option: expected one of ")) {
|
|
70
|
+
const options = message.replace("Invalid option: expected one of ", "");
|
|
71
|
+
return `${path}: must be one of: ${options.replace(/\|/g, ", ").replace(/"/g, "")}`;
|
|
72
|
+
}
|
|
73
|
+
return `${path}: ${message}`;
|
|
74
|
+
}
|
|
75
|
+
function formatZodError(error) {
|
|
76
|
+
// Filter out "missing required field" errors if there's an "unrecognized key" error at the same path
|
|
77
|
+
// This happens when someone uses wrong field name - we want to show the typo, not the missing field
|
|
78
|
+
const issues = error.issues;
|
|
79
|
+
const unrecognizedPaths = new Set();
|
|
80
|
+
for (const issue of issues) {
|
|
81
|
+
if (issue.code === "unrecognized_keys") {
|
|
82
|
+
const basePath = issue.path.join(".");
|
|
83
|
+
unrecognizedPaths.add(basePath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const filteredIssues = issues.filter((issue) => {
|
|
87
|
+
// Keep unrecognized_keys errors
|
|
88
|
+
if (issue.code === "unrecognized_keys")
|
|
89
|
+
return true;
|
|
90
|
+
// Filter out "required field missing" if there's an unrecognized key at the parent path
|
|
91
|
+
if (issue.code === "invalid_type" &&
|
|
92
|
+
issue.message.includes("received undefined")) {
|
|
93
|
+
const parentPath = issue.path.slice(0, -1).join(".");
|
|
94
|
+
if (unrecognizedPaths.has(parentPath)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
const formatted = filteredIssues.map(formatZodIssue);
|
|
101
|
+
return `Invalid configuration:\n${formatted.map((f) => ` - ${f}`).join("\n")}`;
|
|
102
|
+
}
|
|
103
|
+
export function resolveConfigPath(configPath) {
|
|
104
|
+
return configPath ?? "mcpbox.json";
|
|
105
|
+
}
|
|
106
|
+
export function loadConfig(configPath) {
|
|
107
|
+
if (!existsSync(configPath)) {
|
|
108
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
109
|
+
}
|
|
110
|
+
const content = readFileSync(configPath, "utf-8");
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(content);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
throw new Error(`Invalid JSON in config file: ${e instanceof Error ? e.message : String(e)}`);
|
|
117
|
+
}
|
|
118
|
+
// Substitute environment variables before validation
|
|
119
|
+
const substituted = substituteEnvVars(parsed);
|
|
120
|
+
// Validate with Zod schema
|
|
121
|
+
const result = RawConfigSchema.safeParse(substituted);
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
throw new Error(formatZodError(result.error));
|
|
124
|
+
}
|
|
125
|
+
const raw = result.data;
|
|
126
|
+
const mcps = raw.mcpServers ? parseMcpServers(raw.mcpServers) : [];
|
|
127
|
+
return {
|
|
128
|
+
server: raw.server ?? { port: 8080 },
|
|
129
|
+
auth: raw.auth,
|
|
130
|
+
storage: raw.storage,
|
|
131
|
+
log: raw.log,
|
|
132
|
+
mcps,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* MCP server entry (command + args + env)
|
|
4
|
+
*/
|
|
5
|
+
export declare const McpServerEntrySchema: z.ZodObject<{
|
|
6
|
+
command: z.ZodString;
|
|
7
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
/**
|
|
12
|
+
* OAuth user credentials
|
|
13
|
+
*/
|
|
14
|
+
export declare const OAuthUserSchema: z.ZodObject<{
|
|
15
|
+
username: z.ZodString;
|
|
16
|
+
password: z.ZodString;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
/**
|
|
19
|
+
* OAuth client configuration
|
|
20
|
+
*/
|
|
21
|
+
export declare const OAuthClientSchema: z.ZodObject<{
|
|
22
|
+
client_id: z.ZodString;
|
|
23
|
+
client_name: z.ZodOptional<z.ZodString>;
|
|
24
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
25
|
+
redirect_uris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
|
+
grant_type: z.ZodEnum<{
|
|
27
|
+
authorization_code: "authorization_code";
|
|
28
|
+
client_credentials: "client_credentials";
|
|
29
|
+
}>;
|
|
30
|
+
}, z.core.$strict>;
|
|
31
|
+
/**
|
|
32
|
+
* Auth config - discriminated union based on type
|
|
33
|
+
*/
|
|
34
|
+
export declare const AuthConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
35
|
+
type: z.ZodLiteral<"apikey">;
|
|
36
|
+
apiKey: z.ZodString;
|
|
37
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
38
|
+
type: z.ZodLiteral<"oauth">;
|
|
39
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
40
|
+
users: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
41
|
+
username: z.ZodString;
|
|
42
|
+
password: z.ZodString;
|
|
43
|
+
}, z.core.$strict>>>;
|
|
44
|
+
clients: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
45
|
+
client_id: z.ZodString;
|
|
46
|
+
client_name: z.ZodOptional<z.ZodString>;
|
|
47
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
48
|
+
redirect_uris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
49
|
+
grant_type: z.ZodEnum<{
|
|
50
|
+
authorization_code: "authorization_code";
|
|
51
|
+
client_credentials: "client_credentials";
|
|
52
|
+
}>;
|
|
53
|
+
}, z.core.$strict>>>;
|
|
54
|
+
dynamic_registration: z.ZodOptional<z.ZodBoolean>;
|
|
55
|
+
}, z.core.$strict>], "type">;
|
|
56
|
+
/**
|
|
57
|
+
* Server configuration
|
|
58
|
+
*/
|
|
59
|
+
export declare const ServerConfigSchema: z.ZodObject<{
|
|
60
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
61
|
+
}, z.core.$strict>;
|
|
62
|
+
/**
|
|
63
|
+
* Log configuration
|
|
64
|
+
*/
|
|
65
|
+
export declare const LogConfigSchema: z.ZodObject<{
|
|
66
|
+
level: z.ZodOptional<z.ZodEnum<{
|
|
67
|
+
error: "error";
|
|
68
|
+
debug: "debug";
|
|
69
|
+
info: "info";
|
|
70
|
+
warn: "warn";
|
|
71
|
+
}>>;
|
|
72
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
73
|
+
pretty: "pretty";
|
|
74
|
+
json: "json";
|
|
75
|
+
}>>;
|
|
76
|
+
redactSecrets: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
+
mcpDebug: z.ZodOptional<z.ZodBoolean>;
|
|
78
|
+
}, z.core.$strict>;
|
|
79
|
+
/**
|
|
80
|
+
* Storage configuration - discriminated union based on type
|
|
81
|
+
*/
|
|
82
|
+
export declare const StorageConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
83
|
+
type: z.ZodLiteral<"memory">;
|
|
84
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
85
|
+
type: z.ZodLiteral<"sqlite">;
|
|
86
|
+
path: z.ZodOptional<z.ZodString>;
|
|
87
|
+
}, z.core.$strict>], "type">;
|
|
88
|
+
/**
|
|
89
|
+
* Raw config file schema (before processing)
|
|
90
|
+
*/
|
|
91
|
+
export declare const RawConfigSchema: z.ZodObject<{
|
|
92
|
+
server: z.ZodOptional<z.ZodObject<{
|
|
93
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
94
|
+
}, z.core.$strict>>;
|
|
95
|
+
auth: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
96
|
+
type: z.ZodLiteral<"apikey">;
|
|
97
|
+
apiKey: z.ZodString;
|
|
98
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
99
|
+
type: z.ZodLiteral<"oauth">;
|
|
100
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
101
|
+
users: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
102
|
+
username: z.ZodString;
|
|
103
|
+
password: z.ZodString;
|
|
104
|
+
}, z.core.$strict>>>;
|
|
105
|
+
clients: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
106
|
+
client_id: z.ZodString;
|
|
107
|
+
client_name: z.ZodOptional<z.ZodString>;
|
|
108
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
109
|
+
redirect_uris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
110
|
+
grant_type: z.ZodEnum<{
|
|
111
|
+
authorization_code: "authorization_code";
|
|
112
|
+
client_credentials: "client_credentials";
|
|
113
|
+
}>;
|
|
114
|
+
}, z.core.$strict>>>;
|
|
115
|
+
dynamic_registration: z.ZodOptional<z.ZodBoolean>;
|
|
116
|
+
}, z.core.$strict>], "type">>;
|
|
117
|
+
storage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
118
|
+
type: z.ZodLiteral<"memory">;
|
|
119
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
120
|
+
type: z.ZodLiteral<"sqlite">;
|
|
121
|
+
path: z.ZodOptional<z.ZodString>;
|
|
122
|
+
}, z.core.$strict>], "type">>;
|
|
123
|
+
log: z.ZodOptional<z.ZodObject<{
|
|
124
|
+
level: z.ZodOptional<z.ZodEnum<{
|
|
125
|
+
error: "error";
|
|
126
|
+
debug: "debug";
|
|
127
|
+
info: "info";
|
|
128
|
+
warn: "warn";
|
|
129
|
+
}>>;
|
|
130
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
131
|
+
pretty: "pretty";
|
|
132
|
+
json: "json";
|
|
133
|
+
}>>;
|
|
134
|
+
redactSecrets: z.ZodOptional<z.ZodBoolean>;
|
|
135
|
+
mcpDebug: z.ZodOptional<z.ZodBoolean>;
|
|
136
|
+
}, z.core.$strict>>;
|
|
137
|
+
mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
138
|
+
command: z.ZodString;
|
|
139
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
140
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
141
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
142
|
+
}, z.core.$strict>>>;
|
|
143
|
+
}, z.core.$strict>;
|
|
144
|
+
/**
|
|
145
|
+
* Internal MCP config (with name resolved from key)
|
|
146
|
+
*/
|
|
147
|
+
export declare const McpConfigSchema: z.ZodObject<{
|
|
148
|
+
name: z.ZodString;
|
|
149
|
+
command: z.ZodString;
|
|
150
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
151
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
152
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
153
|
+
}, z.core.$strip>;
|
|
154
|
+
/**
|
|
155
|
+
* Processed config (after loader adds defaults and resolves mcpServers)
|
|
156
|
+
*/
|
|
157
|
+
export declare const ConfigSchema: z.ZodObject<{
|
|
158
|
+
server: z.ZodObject<{
|
|
159
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
160
|
+
}, z.core.$strict>;
|
|
161
|
+
auth: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
162
|
+
type: z.ZodLiteral<"apikey">;
|
|
163
|
+
apiKey: z.ZodString;
|
|
164
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
165
|
+
type: z.ZodLiteral<"oauth">;
|
|
166
|
+
issuer: z.ZodOptional<z.ZodString>;
|
|
167
|
+
users: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
168
|
+
username: z.ZodString;
|
|
169
|
+
password: z.ZodString;
|
|
170
|
+
}, z.core.$strict>>>;
|
|
171
|
+
clients: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
172
|
+
client_id: z.ZodString;
|
|
173
|
+
client_name: z.ZodOptional<z.ZodString>;
|
|
174
|
+
client_secret: z.ZodOptional<z.ZodString>;
|
|
175
|
+
redirect_uris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
176
|
+
grant_type: z.ZodEnum<{
|
|
177
|
+
authorization_code: "authorization_code";
|
|
178
|
+
client_credentials: "client_credentials";
|
|
179
|
+
}>;
|
|
180
|
+
}, z.core.$strict>>>;
|
|
181
|
+
dynamic_registration: z.ZodOptional<z.ZodBoolean>;
|
|
182
|
+
}, z.core.$strict>], "type">>;
|
|
183
|
+
storage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
184
|
+
type: z.ZodLiteral<"memory">;
|
|
185
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
186
|
+
type: z.ZodLiteral<"sqlite">;
|
|
187
|
+
path: z.ZodOptional<z.ZodString>;
|
|
188
|
+
}, z.core.$strict>], "type">>;
|
|
189
|
+
log: z.ZodOptional<z.ZodObject<{
|
|
190
|
+
level: z.ZodOptional<z.ZodEnum<{
|
|
191
|
+
error: "error";
|
|
192
|
+
debug: "debug";
|
|
193
|
+
info: "info";
|
|
194
|
+
warn: "warn";
|
|
195
|
+
}>>;
|
|
196
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
197
|
+
pretty: "pretty";
|
|
198
|
+
json: "json";
|
|
199
|
+
}>>;
|
|
200
|
+
redactSecrets: z.ZodOptional<z.ZodBoolean>;
|
|
201
|
+
mcpDebug: z.ZodOptional<z.ZodBoolean>;
|
|
202
|
+
}, z.core.$strict>>;
|
|
203
|
+
mcps: z.ZodArray<z.ZodObject<{
|
|
204
|
+
name: z.ZodString;
|
|
205
|
+
command: z.ZodString;
|
|
206
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
207
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
208
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
209
|
+
}, z.core.$strip>>;
|
|
210
|
+
}, z.core.$strip>;
|
|
211
|
+
export type McpServerEntry = z.infer<typeof McpServerEntrySchema>;
|
|
212
|
+
export type OAuthUser = z.infer<typeof OAuthUserSchema>;
|
|
213
|
+
export type OAuthClient = z.infer<typeof OAuthClientSchema>;
|
|
214
|
+
export type AuthConfig = z.infer<typeof AuthConfigSchema>;
|
|
215
|
+
export type ServerConfig = z.infer<typeof ServerConfigSchema>;
|
|
216
|
+
export type LogConfig = z.infer<typeof LogConfigSchema>;
|
|
217
|
+
export type StorageConfig = z.infer<typeof StorageConfigSchema>;
|
|
218
|
+
export type RawConfig = z.infer<typeof RawConfigSchema>;
|
|
219
|
+
export type McpConfig = z.infer<typeof McpConfigSchema>;
|
|
220
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* API key format: 16-128 characters, alphanumeric with hyphens and underscores.
|
|
4
|
+
*/
|
|
5
|
+
const ApiKeySchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.min(16, "API key must be at least 16 characters")
|
|
8
|
+
.max(128, "API key must be at most 128 characters")
|
|
9
|
+
.regex(/^[A-Za-z0-9_-]+$/, "API key must contain only A-Z, a-z, 0-9, hyphens, and underscores");
|
|
10
|
+
/**
|
|
11
|
+
* MCP server entry (command + args + env)
|
|
12
|
+
*/
|
|
13
|
+
export const McpServerEntrySchema = z
|
|
14
|
+
.object({
|
|
15
|
+
command: z.string().min(1, "Command is required"),
|
|
16
|
+
args: z.array(z.string()).optional(),
|
|
17
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
18
|
+
tools: z.array(z.string()).optional(),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
/**
|
|
22
|
+
* OAuth user credentials
|
|
23
|
+
*/
|
|
24
|
+
export const OAuthUserSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
username: z.string().min(1, "Username is required"),
|
|
27
|
+
password: z.string().min(1, "Password is required"),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
/**
|
|
31
|
+
* OAuth client configuration
|
|
32
|
+
*/
|
|
33
|
+
export const OAuthClientSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
client_id: z.string().min(1, "Client ID is required"),
|
|
36
|
+
client_name: z.string().optional(),
|
|
37
|
+
client_secret: z.string().optional(),
|
|
38
|
+
redirect_uris: z.array(z.string().url("Invalid redirect URI")).optional(),
|
|
39
|
+
grant_type: z.enum(["authorization_code", "client_credentials"]),
|
|
40
|
+
})
|
|
41
|
+
.strict()
|
|
42
|
+
.refine((client) => {
|
|
43
|
+
// client_credentials requires client_secret
|
|
44
|
+
if (client.grant_type === "client_credentials" && !client.client_secret) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}, {
|
|
49
|
+
message: "client_secret is required for client_credentials grant type",
|
|
50
|
+
})
|
|
51
|
+
.refine((client) => {
|
|
52
|
+
// authorization_code requires redirect_uris
|
|
53
|
+
if (client.grant_type === "authorization_code" &&
|
|
54
|
+
(!client.redirect_uris || client.redirect_uris.length === 0)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}, {
|
|
59
|
+
message: "redirect_uris is required for authorization_code grant type",
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Auth config - discriminated union based on type
|
|
63
|
+
*/
|
|
64
|
+
export const AuthConfigSchema = z.discriminatedUnion("type", [
|
|
65
|
+
// API key authentication
|
|
66
|
+
z
|
|
67
|
+
.object({
|
|
68
|
+
type: z.literal("apikey"),
|
|
69
|
+
apiKey: ApiKeySchema,
|
|
70
|
+
})
|
|
71
|
+
.strict(),
|
|
72
|
+
// OAuth authentication
|
|
73
|
+
z
|
|
74
|
+
.object({
|
|
75
|
+
type: z.literal("oauth"),
|
|
76
|
+
issuer: z.string().url("Invalid issuer URL").optional(),
|
|
77
|
+
users: z.array(OAuthUserSchema).optional(),
|
|
78
|
+
clients: z.array(OAuthClientSchema).optional(),
|
|
79
|
+
dynamic_registration: z.boolean().optional(),
|
|
80
|
+
})
|
|
81
|
+
.strict()
|
|
82
|
+
.refine((oauth) => {
|
|
83
|
+
// Must have at least users, clients, or dynamic_registration
|
|
84
|
+
const hasUsers = oauth.users && oauth.users.length > 0;
|
|
85
|
+
const hasClients = oauth.clients && oauth.clients.length > 0;
|
|
86
|
+
const hasDynamicReg = oauth.dynamic_registration === true;
|
|
87
|
+
return hasUsers || hasClients || hasDynamicReg;
|
|
88
|
+
}, {
|
|
89
|
+
message: "OAuth requires at least one of: users, clients, or dynamic_registration enabled",
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
/**
|
|
93
|
+
* Server configuration
|
|
94
|
+
*/
|
|
95
|
+
export const ServerConfigSchema = z
|
|
96
|
+
.object({
|
|
97
|
+
port: z
|
|
98
|
+
.number()
|
|
99
|
+
.int()
|
|
100
|
+
.min(1, "Port must be at least 1")
|
|
101
|
+
.max(65535, "Port must be at most 65535")
|
|
102
|
+
.default(8080),
|
|
103
|
+
})
|
|
104
|
+
.strict();
|
|
105
|
+
/**
|
|
106
|
+
* Log configuration
|
|
107
|
+
*/
|
|
108
|
+
export const LogConfigSchema = z
|
|
109
|
+
.object({
|
|
110
|
+
level: z.enum(["debug", "info", "warn", "error"]).optional(),
|
|
111
|
+
format: z.enum(["pretty", "json"]).optional(),
|
|
112
|
+
redactSecrets: z.boolean().optional(),
|
|
113
|
+
mcpDebug: z.boolean().optional(),
|
|
114
|
+
})
|
|
115
|
+
.strict();
|
|
116
|
+
/**
|
|
117
|
+
* Storage configuration - discriminated union based on type
|
|
118
|
+
*/
|
|
119
|
+
export const StorageConfigSchema = z.discriminatedUnion("type", [
|
|
120
|
+
z.object({ type: z.literal("memory") }).strict(),
|
|
121
|
+
z
|
|
122
|
+
.object({
|
|
123
|
+
type: z.literal("sqlite"),
|
|
124
|
+
path: z.string().optional(),
|
|
125
|
+
})
|
|
126
|
+
.strict(),
|
|
127
|
+
]);
|
|
128
|
+
/**
|
|
129
|
+
* Raw config file schema (before processing)
|
|
130
|
+
*/
|
|
131
|
+
export const RawConfigSchema = z
|
|
132
|
+
.object({
|
|
133
|
+
server: ServerConfigSchema.optional(),
|
|
134
|
+
auth: AuthConfigSchema.optional(),
|
|
135
|
+
storage: StorageConfigSchema.optional(),
|
|
136
|
+
log: LogConfigSchema.optional(),
|
|
137
|
+
mcpServers: z.record(z.string(), McpServerEntrySchema).optional(),
|
|
138
|
+
})
|
|
139
|
+
.strict();
|
|
140
|
+
/**
|
|
141
|
+
* Internal MCP config (with name resolved from key)
|
|
142
|
+
*/
|
|
143
|
+
export const McpConfigSchema = z.object({
|
|
144
|
+
name: z.string(),
|
|
145
|
+
command: z.string(),
|
|
146
|
+
args: z.array(z.string()).optional(),
|
|
147
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
148
|
+
tools: z.array(z.string()).optional(),
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* Processed config (after loader adds defaults and resolves mcpServers)
|
|
152
|
+
*/
|
|
153
|
+
export const ConfigSchema = z.object({
|
|
154
|
+
server: ServerConfigSchema,
|
|
155
|
+
auth: AuthConfigSchema.optional(),
|
|
156
|
+
storage: StorageConfigSchema.optional(),
|
|
157
|
+
log: LogConfigSchema.optional(),
|
|
158
|
+
mcps: z.array(McpConfigSchema),
|
|
159
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { AuthConfig, Config, 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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig, resolveConfigPath } from "./config/loader.js";
|
|
3
|
+
import { configureLogger, logger } from "./logger.js";
|
|
4
|
+
import { createServer } from "./server.js";
|
|
5
|
+
import { VERSION } from "./version.js";
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(`
|
|
8
|
+
mcpbox - Expose MCP servers via HTTP
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
mcpbox [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-c, --config <path> Path to config file (default: mcpbox.json)
|
|
15
|
+
-h, --help Show this help message
|
|
16
|
+
-v, --version Show version
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
function parseArgs(args) {
|
|
20
|
+
const result = {
|
|
21
|
+
config: undefined,
|
|
22
|
+
help: false,
|
|
23
|
+
version: false,
|
|
24
|
+
};
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg === "-h" || arg === "--help") {
|
|
28
|
+
result.help = true;
|
|
29
|
+
}
|
|
30
|
+
else if (arg === "-v" || arg === "--version") {
|
|
31
|
+
result.version = true;
|
|
32
|
+
}
|
|
33
|
+
else if (arg === "-c" || arg === "--config") {
|
|
34
|
+
result.config = args[++i];
|
|
35
|
+
}
|
|
36
|
+
else if (!arg.startsWith("-")) {
|
|
37
|
+
// Positional arg = config path (backwards compat)
|
|
38
|
+
result.config = arg;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
const args = parseArgs(process.argv.slice(2));
|
|
44
|
+
if (args.help) {
|
|
45
|
+
printHelp();
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
if (args.version) {
|
|
49
|
+
console.log(VERSION);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
const configPath = resolveConfigPath(args.config);
|
|
53
|
+
let config;
|
|
54
|
+
try {
|
|
55
|
+
config = loadConfig(configPath);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
59
|
+
// Log to stderr directly since logger config isn't loaded yet
|
|
60
|
+
console.error(`Failed to load config from ${configPath}:\n${message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// Reconfigure logger with settings from config
|
|
64
|
+
configureLogger(config.log);
|
|
65
|
+
logger.info({
|
|
66
|
+
mcps: config.mcps.map((m) => m.name),
|
|
67
|
+
auth: config.auth?.type ?? "none",
|
|
68
|
+
}, "Config loaded");
|
|
69
|
+
let closeServer;
|
|
70
|
+
let isShuttingDown = false;
|
|
71
|
+
async function shutdown(signal) {
|
|
72
|
+
if (isShuttingDown) {
|
|
73
|
+
logger.warn("Shutdown already in progress, forcing exit");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
isShuttingDown = true;
|
|
77
|
+
logger.info(`Received ${signal}, shutting down gracefully...`);
|
|
78
|
+
try {
|
|
79
|
+
if (closeServer) {
|
|
80
|
+
await closeServer();
|
|
81
|
+
}
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.error({
|
|
86
|
+
error: error instanceof Error ? error.message : String(error),
|
|
87
|
+
}, "Error during shutdown");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Graceful shutdown handlers
|
|
92
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
93
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
94
|
+
// Handle uncaught errors
|
|
95
|
+
process.on("uncaughtException", (error) => {
|
|
96
|
+
logger.error({
|
|
97
|
+
error: error.message,
|
|
98
|
+
stack: error.stack,
|
|
99
|
+
}, "Uncaught exception");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
102
|
+
process.on("unhandledRejection", (reason) => {
|
|
103
|
+
logger.error({
|
|
104
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
105
|
+
}, "Unhandled rejection");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
const { close } = await createServer(config);
|
|
110
|
+
closeServer = close;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
logger.error({
|
|
114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
115
|
+
}, "Failed to start server");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
package/dist/logger.d.ts
ADDED