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,3 @@
1
+ import { type Config } from "./schema.js";
2
+ export declare function resolveConfigPath(configPath?: string): string;
3
+ export declare function loadConfig(configPath: string): Config;
@@ -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 {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }
@@ -0,0 +1,5 @@
1
+ import pino from "pino";
2
+ import type { LogConfig } from "./config/types.js";
3
+ export declare function redactSensitiveStrings(obj: unknown): unknown;
4
+ export declare let logger: pino.Logger;
5
+ export declare function configureLogger(config?: LogConfig): void;