first commit

This commit is contained in:
kst
2026-08-19 22:52:46 +09:00
commit 8454476c93
30 changed files with 7352 additions and 0 deletions
+200
View File
@@ -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,
),
};
}