172 lines
4.9 KiB
TypeScript
172 lines
4.9 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { isAbsolute, resolve } from "node:path";
|
|
import type { CodexOptions } from "@openai/codex-sdk";
|
|
import { getConfig, type AppConfig } from "./config.js";
|
|
import { normalizeSharedMcpConfigFile } from "./mcp/converters.js";
|
|
import {
|
|
createDefaultMcpRegistry,
|
|
createMcpHandlerShell,
|
|
type McpHandlerBusinessLogic,
|
|
type McpHandlerBusinessLogicInput,
|
|
type McpHandlerInput,
|
|
type McpHandlerResult,
|
|
type McpHandlerShellOptions,
|
|
type McpHandlerUtils,
|
|
McpRegistry,
|
|
type McpServerHandler,
|
|
} from "./mcp/handlers.js";
|
|
import type {
|
|
LoadedMcpConfig,
|
|
McpLoadContext,
|
|
SharedMcpConfigFile,
|
|
} from "./mcp/types.js";
|
|
import { parseMcpConfig } from "./mcp/types.js";
|
|
import type { ToolClearancePolicy } from "./security/schemas.js";
|
|
|
|
function readConfigFile(input: {
|
|
configPath: string;
|
|
workingDirectory?: string;
|
|
}): {
|
|
config?: SharedMcpConfigFile;
|
|
sourcePath?: string;
|
|
} {
|
|
const candidatePath = input.configPath.trim() || "./mcp.config.json";
|
|
const resolvedPath = isAbsolute(candidatePath)
|
|
? candidatePath
|
|
: resolve(input.workingDirectory ?? process.cwd(), candidatePath);
|
|
|
|
if (!existsSync(resolvedPath)) {
|
|
if (candidatePath !== "./mcp.config.json") {
|
|
throw new Error(`MCP config file not found: ${resolvedPath}`);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
const rawText = readFileSync(resolvedPath, "utf8");
|
|
const parsed = parseMcpConfig(JSON.parse(rawText) as unknown);
|
|
|
|
return {
|
|
config: normalizeSharedMcpConfigFile(parsed),
|
|
sourcePath: resolvedPath,
|
|
};
|
|
}
|
|
|
|
function warnClaudeUnsupportedSharedFields(
|
|
config: SharedMcpConfigFile,
|
|
warn: (message: string) => void,
|
|
): void {
|
|
for (const [serverName, server] of Object.entries(config.servers ?? {})) {
|
|
if (server.enabled_tools) {
|
|
warn(
|
|
`[WARN] MCP field 'enabled_tools' is not supported by the Claude adapter and will be ignored (server: "${serverName}").`,
|
|
);
|
|
}
|
|
if (server.startup_timeout_sec !== undefined || server.tool_timeout_sec !== undefined) {
|
|
warn(
|
|
`[WARN] MCP field 'timeouts' is not supported by the Claude adapter and will be ignored (server: "${serverName}").`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const defaultMcpRegistry = createDefaultMcpRegistry();
|
|
|
|
export function getDefaultMcpRegistry(): McpRegistry {
|
|
return defaultMcpRegistry;
|
|
}
|
|
|
|
export function loadMcpConfigFromEnv(
|
|
context: McpLoadContext = {},
|
|
options?: {
|
|
config?: Readonly<AppConfig>;
|
|
registry?: McpRegistry;
|
|
toolClearance?: ToolClearancePolicy;
|
|
warn?: (message: string) => void;
|
|
},
|
|
): LoadedMcpConfig {
|
|
const runtimeConfig = options?.config ?? getConfig();
|
|
const registry = options?.registry ?? defaultMcpRegistry;
|
|
const warn = options?.warn ?? ((message: string) => console.warn(message));
|
|
|
|
const { config, sourcePath } = readConfigFile({
|
|
configPath: runtimeConfig.mcp.configPath,
|
|
workingDirectory: context.workingDirectory,
|
|
});
|
|
if (!config) {
|
|
return {};
|
|
}
|
|
|
|
if (context.providerHint === "claude" || context.providerHint === "both") {
|
|
warnClaudeUnsupportedSharedFields(config, warn);
|
|
}
|
|
|
|
const codexServers: NonNullable<CodexOptions["config"]> = {};
|
|
const claudeServers: NonNullable<LoadedMcpConfig["claudeMcpServers"]> = {};
|
|
const resolvedHandlers: Record<string, string> = {};
|
|
|
|
for (const [serverName, server] of Object.entries(config.servers ?? {})) {
|
|
const resolved = registry.resolveServerWithHandler({
|
|
serverName,
|
|
server,
|
|
context,
|
|
fullConfig: config,
|
|
toolClearance: options?.toolClearance,
|
|
});
|
|
resolvedHandlers[serverName] = resolved.handlerId;
|
|
|
|
if (resolved.enabled === false) {
|
|
continue;
|
|
}
|
|
if (resolved.codex) {
|
|
codexServers[serverName] = resolved.codex;
|
|
}
|
|
if (resolved.claude) {
|
|
claudeServers[serverName] = resolved.claude;
|
|
}
|
|
}
|
|
|
|
const codexWithOverrides = {
|
|
...codexServers,
|
|
...(config.codex?.mcp_servers ?? {}),
|
|
};
|
|
const claudeWithOverrides = {
|
|
...claudeServers,
|
|
...(config.claude?.mcpServers ?? {}),
|
|
};
|
|
|
|
const codexConfig =
|
|
Object.keys(codexWithOverrides).length > 0
|
|
? ({ mcp_servers: codexWithOverrides } as NonNullable<CodexOptions["config"]>)
|
|
: undefined;
|
|
|
|
return {
|
|
...(codexConfig ? { codexConfig } : {}),
|
|
...(Object.keys(claudeWithOverrides).length > 0
|
|
? { claudeMcpServers: claudeWithOverrides }
|
|
: {}),
|
|
...(sourcePath ? { sourcePath } : {}),
|
|
...(Object.keys(resolvedHandlers).length > 0 ? { resolvedHandlers } : {}),
|
|
};
|
|
}
|
|
|
|
export function registerMcpHandler(handler: McpServerHandler): void {
|
|
defaultMcpRegistry.register(handler);
|
|
}
|
|
|
|
export function listMcpHandlers(): McpServerHandler[] {
|
|
return defaultMcpRegistry.listHandlers();
|
|
}
|
|
|
|
export { createDefaultMcpRegistry, createMcpHandlerShell, McpRegistry };
|
|
export type {
|
|
LoadedMcpConfig,
|
|
McpHandlerBusinessLogic,
|
|
McpHandlerBusinessLogicInput,
|
|
McpHandlerInput,
|
|
McpHandlerResult,
|
|
McpHandlerShellOptions,
|
|
McpHandlerUtils,
|
|
McpLoadContext,
|
|
McpServerHandler,
|
|
};
|