first commit
This commit is contained in:
76
src/mcp/converters.ts
Normal file
76
src/mcp/converters.ts
Normal 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
233
src/mcp/handlers.ts
Normal 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
55
src/mcp/types.ts
Normal 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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user