first commit

This commit is contained in:
2026-02-23 12:06:13 -05:00
commit 53af0d44cd
33 changed files with 6483 additions and 0 deletions

76
src/mcp/converters.ts Normal file
View File

@@ -0,0 +1,76 @@
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { CodexConfigObject, SharedMcpServer, Transport } from "./types.js";
export function inferTransport(server: SharedMcpServer): Transport {
if (server.type) {
return server.type;
}
return server.url ? "http" : "stdio";
}
export function toCodexServerConfig(serverName: string, server: SharedMcpServer): CodexConfigObject {
const type = inferTransport(server);
if (type === "stdio" && !server.command) {
throw new Error(`Shared MCP server "${serverName}" requires "command" for stdio transport.`);
}
if ((type === "http" || type === "sse") && !server.url) {
throw new Error(`Shared MCP server "${serverName}" requires "url" for ${type} transport.`);
}
const config: CodexConfigObject = {};
if (server.command) config.command = server.command;
if (server.args) config.args = server.args;
if (server.env) config.env = server.env;
if (server.cwd) config.cwd = server.cwd;
if (server.url) config.url = server.url;
if (server.enabled !== undefined) config.enabled = server.enabled;
if (server.required !== undefined) config.required = server.required;
if (server.enabled_tools) config.enabled_tools = server.enabled_tools;
if (server.disabled_tools) config.disabled_tools = server.disabled_tools;
if (server.startup_timeout_sec !== undefined) {
config.startup_timeout_sec = server.startup_timeout_sec;
}
if (server.tool_timeout_sec !== undefined) {
config.tool_timeout_sec = server.tool_timeout_sec;
}
if (server.bearer_token_env_var) {
config.bearer_token_env_var = server.bearer_token_env_var;
}
const httpHeaders = server.http_headers ?? server.headers;
if (httpHeaders) {
config.http_headers = httpHeaders;
}
if (server.env_http_headers) config.env_http_headers = server.env_http_headers;
if (server.env_vars) config.env_vars = server.env_vars;
return config;
}
export function toClaudeServerConfig(serverName: string, server: SharedMcpServer): McpServerConfig {
const type = inferTransport(server);
if (type === "stdio") {
if (!server.command) {
throw new Error(`Shared MCP server "${serverName}" requires "command" for stdio transport.`);
}
return {
type: "stdio",
command: server.command,
...(server.args ? { args: server.args } : {}),
...(server.env ? { env: server.env } : {}),
};
}
if (!server.url) {
throw new Error(`Shared MCP server "${serverName}" requires "url" for ${type} transport.`);
}
return {
type,
url: server.url,
...(server.headers ? { headers: server.headers } : {}),
};
}

233
src/mcp/handlers.ts Normal file
View File

@@ -0,0 +1,233 @@
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import {
inferTransport,
toClaudeServerConfig,
toCodexServerConfig,
} from "./converters.js";
import type {
CodexConfigObject,
McpLoadContext,
SharedMcpConfigFile,
SharedMcpServer,
} from "./types.js";
export type McpHandlerUtils = {
inferTransport: typeof inferTransport;
toCodexServerConfig: typeof toCodexServerConfig;
toClaudeServerConfig: typeof toClaudeServerConfig;
};
export type McpHandlerInput = {
serverName: string;
server: SharedMcpServer;
context: McpLoadContext;
handlerConfig: Record<string, unknown>;
fullConfig: SharedMcpConfigFile;
utils: McpHandlerUtils;
};
export type McpHandlerResult = {
enabled?: boolean;
codex?: CodexConfigObject;
claude?: McpServerConfig;
};
export type McpServerHandler = {
id: string;
description: string;
matches: (input: Pick<McpHandlerInput, "serverName" | "server">) => boolean;
resolve: (input: McpHandlerInput) => McpHandlerResult;
};
export type McpHandlerBusinessLogicInput = McpHandlerInput & {
baseResult: McpHandlerResult;
};
export type McpHandlerBusinessLogic = (
input: McpHandlerBusinessLogicInput,
) => McpHandlerResult | void;
export type McpHandlerShellOptions = {
id: string;
description: string;
matches: McpServerHandler["matches"];
applyBusinessLogic?: McpHandlerBusinessLogic;
};
const utils: McpHandlerUtils = {
inferTransport,
toCodexServerConfig,
toClaudeServerConfig,
};
function createDefaultResult({
serverName,
server,
localUtils,
}: {
serverName: string;
server: SharedMcpServer;
localUtils: McpHandlerUtils;
}): McpHandlerResult {
return {
codex: localUtils.toCodexServerConfig(serverName, server),
claude: localUtils.toClaudeServerConfig(serverName, server),
};
}
export function createMcpHandlerShell(options: McpHandlerShellOptions): McpServerHandler {
const { id, description, matches, applyBusinessLogic } = options;
return {
id,
description,
matches,
resolve: (input) => {
const baseResult = createDefaultResult({
serverName: input.serverName,
server: input.server,
localUtils: input.utils,
});
const overridden = applyBusinessLogic?.({
...input,
baseResult,
});
return overridden ?? baseResult;
},
};
}
function isNamedLike(
input: Pick<McpHandlerInput, "serverName" | "server">,
patterns: string[],
): boolean {
const values = [input.serverName, input.server.command, input.server.url]
.filter((value): value is string => Boolean(value))
.map((value) => value.toLowerCase());
return patterns.some((pattern) => values.some((value) => value.includes(pattern)));
}
function readBooleanConfigValue(
config: Record<string, unknown>,
key: string,
): boolean | undefined {
const value = config[key];
return typeof value === "boolean" ? value : undefined;
}
function applyEnabledByDefault(input: McpHandlerBusinessLogicInput): McpHandlerResult {
if (input.server.enabled !== undefined) {
return input.baseResult;
}
const enabledByDefault = readBooleanConfigValue(input.handlerConfig, "enabledByDefault");
return enabledByDefault === false
? {
...input.baseResult,
enabled: false,
}
: input.baseResult;
}
const context7Handler = createMcpHandlerShell({
id: "context7",
description:
"Dedicated extension point for Context7 policy/behavior. Business logic belongs in applyBusinessLogic.",
matches: (input) => isNamedLike(input, ["context7"]),
applyBusinessLogic: applyEnabledByDefault,
});
const claudeTaskMasterHandler = createMcpHandlerShell({
id: "claude-task-master",
description:
"Dedicated extension point for Claude Task Master policy/behavior. Business logic belongs in applyBusinessLogic.",
matches: (input) =>
isNamedLike(input, ["claude-task-master", "task-master", "taskmaster"]),
applyBusinessLogic: applyEnabledByDefault,
});
const genericHandler: McpServerHandler = {
id: "generic",
description: "Default passthrough mapping for project-specific MCP servers.",
matches: () => true,
resolve: ({ serverName, server, utils: localUtils }) =>
createDefaultResult({ serverName, server, localUtils }),
};
const handlerRegistry = new Map<string, McpServerHandler>();
const handlerOrder: string[] = [];
function installBuiltinHandlers(): void {
registerMcpHandler(context7Handler);
registerMcpHandler(claudeTaskMasterHandler);
registerMcpHandler(genericHandler);
}
export function registerMcpHandler(handler: McpServerHandler): void {
if (handlerRegistry.has(handler.id)) {
handlerRegistry.set(handler.id, handler);
return;
}
handlerRegistry.set(handler.id, handler);
handlerOrder.push(handler.id);
}
export function listMcpHandlers(): McpServerHandler[] {
return handlerOrder
.map((id) => handlerRegistry.get(id))
.filter((handler): handler is McpServerHandler => Boolean(handler));
}
function resolveHandler(serverName: string, server: SharedMcpServer): McpServerHandler {
if (server.handler) {
const explicit = handlerRegistry.get(server.handler);
if (!explicit) {
throw new Error(
`Unknown MCP handler "${server.handler}" configured for server "${serverName}".`,
);
}
return explicit;
}
for (const id of handlerOrder) {
const handler = handlerRegistry.get(id);
if (!handler || id === "generic") {
continue;
}
if (handler.matches({ serverName, server })) {
return handler;
}
}
const fallback = handlerRegistry.get("generic");
if (!fallback) {
throw new Error('No MCP fallback handler registered. Expected handler id "generic".');
}
return fallback;
}
export function resolveServerWithHandler(input: {
serverName: string;
server: SharedMcpServer;
context: McpLoadContext;
fullConfig: SharedMcpConfigFile;
}): McpHandlerResult & { handlerId: string } {
const { serverName, server, context, fullConfig } = input;
const handler = resolveHandler(serverName, server);
const handlerConfig = {
...(fullConfig.handlerSettings?.[handler.id] ?? {}),
...(server.handlerOptions ?? {}),
};
const result = handler.resolve({
serverName,
server,
context,
handlerConfig,
fullConfig,
utils,
});
return {
...result,
handlerId: handler.id,
};
}
installBuiltinHandlers();

55
src/mcp/types.ts Normal file
View File

@@ -0,0 +1,55 @@
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
import type { CodexOptions } from "@openai/codex-sdk";
export type Transport = "stdio" | "http" | "sse";
export type CodexConfigValue = string | number | boolean | CodexConfigValue[] | CodexConfigObject;
export type CodexConfigObject = {
[key: string]: CodexConfigValue;
};
export type SharedMcpServer = {
type?: Transport;
command?: string;
args?: string[];
env?: Record<string, string>;
cwd?: string;
url?: string;
headers?: Record<string, string>;
enabled?: boolean;
required?: boolean;
enabled_tools?: string[];
disabled_tools?: string[];
startup_timeout_sec?: number;
tool_timeout_sec?: number;
bearer_token_env_var?: string;
http_headers?: Record<string, string>;
env_http_headers?: Record<string, string>;
env_vars?: string[];
handler?: string;
handlerOptions?: Record<string, unknown>;
};
export type SharedMcpConfigFile = {
servers?: Record<string, SharedMcpServer>;
codex?: {
mcp_servers?: Record<string, CodexConfigObject>;
};
claude?: {
mcpServers?: Record<string, McpServerConfig>;
};
handlerSettings?: Record<string, Record<string, unknown>>;
};
export type McpLoadContext = {
providerHint?: "codex" | "claude" | "both";
prompt?: string;
};
export type LoadedMcpConfig = {
codexConfig?: NonNullable<CodexOptions["config"]>;
claudeMcpServers?: Record<string, McpServerConfig>;
sourcePath?: string;
resolvedHandlers?: Record<string, string>;
};