first commit
This commit is contained in:
+200
@@ -0,0 +1,200 @@
|
||||
import path from "node:path";
|
||||
|
||||
export interface AppConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
endpoint: string;
|
||||
publicUrl: string | undefined;
|
||||
allowedHosts: string[] | undefined;
|
||||
authToken: string | undefined;
|
||||
allowNoAuth: boolean;
|
||||
oauthEnabled: boolean;
|
||||
oauthIssuerUrl: string | undefined;
|
||||
oauthResourceUrl: string | undefined;
|
||||
oauthStateFile: string;
|
||||
oauthAccessTokenTtlSeconds: number;
|
||||
oauthRefreshTokenTtlSeconds: number;
|
||||
oauthAuthorizationCodeTtlSeconds: number;
|
||||
defaultCwd: string;
|
||||
defaultShell: string;
|
||||
maxRequestBody: string;
|
||||
maxOutputBytes: number;
|
||||
maxRetainedProcessOutputBytes: number;
|
||||
processRetentionMs: number;
|
||||
maxProcesses: number;
|
||||
sessionTtlMs: number;
|
||||
maxFileChunkBytes: number;
|
||||
maxEditFileBytes: number;
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
||||
if (value === undefined || value === "") {
|
||||
return fallback;
|
||||
}
|
||||
if (["1", "true", "yes", "on"].includes(value.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
if (["0", "false", "no", "off"].includes(value.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
throw new Error(`Invalid boolean value: ${value}`);
|
||||
}
|
||||
|
||||
function parseInteger(
|
||||
value: string | undefined,
|
||||
fallback: number,
|
||||
name: string,
|
||||
minimum: number,
|
||||
): number {
|
||||
if (value === undefined || value === "") {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < minimum) {
|
||||
throw new Error(`${name} must be an integer greater than or equal to ${minimum}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeEndpoint(value: string | undefined): string {
|
||||
const endpoint = value?.trim() || "/mcp";
|
||||
if (!endpoint.startsWith("/")) {
|
||||
throw new Error("MCP_ENDPOINT must start with '/'");
|
||||
}
|
||||
return endpoint.length > 1 ? endpoint.replace(/\/+$/, "") : endpoint;
|
||||
}
|
||||
|
||||
function normalizeOAuthUrl(value: string | undefined, name: string): string {
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required when MCP_OAUTH_ENABLED=true`);
|
||||
}
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new Error(`${name} must be an absolute URL`);
|
||||
}
|
||||
const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
||||
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
|
||||
throw new Error(`${name} must use HTTPS (HTTP is allowed only for loopback tests)`);
|
||||
}
|
||||
if (url.search || url.hash) {
|
||||
throw new Error(`${name} must not contain a query string or fragment`);
|
||||
}
|
||||
return url.href;
|
||||
}
|
||||
|
||||
export function loadConfig(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
processCwd = process.cwd(),
|
||||
): AppConfig {
|
||||
const allowNoAuth = parseBoolean(env.MCP_ALLOW_NO_AUTH, false);
|
||||
const authToken = env.MCP_AUTH_TOKEN?.trim() || undefined;
|
||||
const oauthEnabled = parseBoolean(env.MCP_OAUTH_ENABLED, false);
|
||||
if (!allowNoAuth && !authToken) {
|
||||
throw new Error(
|
||||
"MCP_AUTH_TOKEN is required. Set MCP_ALLOW_NO_AUTH=true only when an upstream OAuth gateway or private network authenticates callers.",
|
||||
);
|
||||
}
|
||||
if (oauthEnabled && !authToken) {
|
||||
throw new Error("MCP_AUTH_TOKEN is required as the OAuth authorization access key");
|
||||
}
|
||||
|
||||
const defaultCwd = path.resolve(env.MCP_DEFAULT_CWD?.trim() || processCwd);
|
||||
const allowedHosts = env.MCP_ALLOWED_HOSTS?.split(",")
|
||||
.map((host) => host.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
const endpoint = normalizeEndpoint(env.MCP_ENDPOINT);
|
||||
const publicUrl = env.MCP_PUBLIC_URL?.trim().replace(/\/+$/, "") || undefined;
|
||||
const oauthIssuerUrl = oauthEnabled
|
||||
? normalizeOAuthUrl(env.MCP_OAUTH_ISSUER?.trim() || publicUrl, "MCP_OAUTH_ISSUER")
|
||||
: undefined;
|
||||
const oauthResourceUrl = oauthEnabled
|
||||
? normalizeOAuthUrl(
|
||||
env.MCP_OAUTH_RESOURCE?.trim() || (publicUrl ? `${publicUrl}${endpoint}` : undefined),
|
||||
"MCP_OAUTH_RESOURCE",
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
host: env.MCP_HOST?.trim() || "0.0.0.0",
|
||||
port: parseInteger(env.MCP_PORT, 3000, "MCP_PORT", 1),
|
||||
endpoint,
|
||||
publicUrl,
|
||||
allowedHosts: allowedHosts && allowedHosts.length > 0 ? allowedHosts : undefined,
|
||||
authToken,
|
||||
allowNoAuth,
|
||||
oauthEnabled,
|
||||
oauthIssuerUrl,
|
||||
oauthResourceUrl,
|
||||
oauthStateFile: path.resolve(
|
||||
env.MCP_OAUTH_STATE_FILE?.trim() ||
|
||||
path.join(processCwd, ".remote-dev-mcp-oauth-state.json"),
|
||||
),
|
||||
oauthAccessTokenTtlSeconds: parseInteger(
|
||||
env.MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
|
||||
60 * 60,
|
||||
"MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS",
|
||||
300,
|
||||
),
|
||||
oauthRefreshTokenTtlSeconds: parseInteger(
|
||||
env.MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS,
|
||||
30 * 24 * 60 * 60,
|
||||
"MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS",
|
||||
3600,
|
||||
),
|
||||
oauthAuthorizationCodeTtlSeconds: parseInteger(
|
||||
env.MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS,
|
||||
5 * 60,
|
||||
"MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS",
|
||||
60,
|
||||
),
|
||||
defaultCwd,
|
||||
defaultShell:
|
||||
env.MCP_DEFAULT_SHELL?.trim() || env.SHELL?.trim() || "/bin/bash",
|
||||
maxRequestBody: env.MCP_MAX_REQUEST_BODY?.trim() || "8mb",
|
||||
maxOutputBytes: parseInteger(
|
||||
env.MCP_MAX_OUTPUT_BYTES,
|
||||
1024 * 1024,
|
||||
"MCP_MAX_OUTPUT_BYTES",
|
||||
16 * 1024,
|
||||
),
|
||||
maxRetainedProcessOutputBytes: parseInteger(
|
||||
env.MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES,
|
||||
4 * 1024 * 1024,
|
||||
"MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES",
|
||||
64 * 1024,
|
||||
),
|
||||
processRetentionMs: parseInteger(
|
||||
env.MCP_PROCESS_RETENTION_MS,
|
||||
60 * 60 * 1000,
|
||||
"MCP_PROCESS_RETENTION_MS",
|
||||
1000,
|
||||
),
|
||||
maxProcesses: parseInteger(
|
||||
env.MCP_MAX_PROCESSES,
|
||||
128,
|
||||
"MCP_MAX_PROCESSES",
|
||||
1,
|
||||
),
|
||||
sessionTtlMs: parseInteger(
|
||||
env.MCP_SESSION_TTL_MS,
|
||||
24 * 60 * 60 * 1000,
|
||||
"MCP_SESSION_TTL_MS",
|
||||
60_000,
|
||||
),
|
||||
maxFileChunkBytes: parseInteger(
|
||||
env.MCP_MAX_FILE_CHUNK_BYTES,
|
||||
1024 * 1024,
|
||||
"MCP_MAX_FILE_CHUNK_BYTES",
|
||||
4096,
|
||||
),
|
||||
maxEditFileBytes: parseInteger(
|
||||
env.MCP_MAX_EDIT_FILE_BYTES,
|
||||
64 * 1024 * 1024,
|
||||
"MCP_MAX_EDIT_FILE_BYTES",
|
||||
4096,
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user