Files
ai_ops/src/mcp.ts

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,
};