Harden MCP schema and wire Claude OAuth/token handling
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type {
|
||||
ClaudeMcpServer,
|
||||
CodexConfigObject,
|
||||
SharedMcpConfigFile,
|
||||
SharedMcpServer,
|
||||
@@ -49,6 +49,10 @@ export function toCodexServerConfig(serverName: string, server: SharedMcpServer)
|
||||
const type = inferTransport(server);
|
||||
const headers = mergeHeaders(server);
|
||||
|
||||
if (type === "sdk") {
|
||||
throw new Error(`Shared MCP server "${serverName}" uses "sdk" transport, which is not supported by Codex.`);
|
||||
}
|
||||
|
||||
if (type === "stdio" && !server.command) {
|
||||
throw new Error(`Shared MCP server "${serverName}" requires "command" for stdio transport.`);
|
||||
}
|
||||
@@ -85,10 +89,20 @@ export function toCodexServerConfig(serverName: string, server: SharedMcpServer)
|
||||
return config;
|
||||
}
|
||||
|
||||
export function toClaudeServerConfig(serverName: string, server: SharedMcpServer): McpServerConfig {
|
||||
export function toClaudeServerConfig(serverName: string, server: SharedMcpServer): ClaudeMcpServer {
|
||||
const type = inferTransport(server);
|
||||
const headers = mergeHeaders(server);
|
||||
|
||||
if (type === "sdk") {
|
||||
if (!server.name) {
|
||||
throw new Error(`Shared MCP server "${serverName}" requires "name" for sdk transport.`);
|
||||
}
|
||||
return {
|
||||
type: "sdk",
|
||||
name: server.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "stdio") {
|
||||
if (!server.command) {
|
||||
throw new Error(`Shared MCP server "${serverName}" requires "command" for stdio transport.`);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
inferTransport,
|
||||
toClaudeServerConfig,
|
||||
toCodexServerConfig,
|
||||
} from "./converters.js";
|
||||
import type {
|
||||
ClaudeMcpServer,
|
||||
CodexConfigObject,
|
||||
McpLoadContext,
|
||||
SharedMcpConfigFile,
|
||||
@@ -33,7 +33,7 @@ export type McpHandlerInput = {
|
||||
export type McpHandlerResult = {
|
||||
enabled?: boolean;
|
||||
codex?: CodexConfigObject;
|
||||
claude?: McpServerConfig;
|
||||
claude?: ClaudeMcpServer;
|
||||
};
|
||||
|
||||
export type McpServerHandler = {
|
||||
|
||||
171
src/mcp/types.ts
171
src/mcp/types.ts
@@ -1,15 +1,22 @@
|
||||
import type { McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type {
|
||||
McpServerConfig,
|
||||
McpServerConfigForProcessTransport,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { CodexOptions } from "@openai/codex-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
export type Transport = "stdio" | "http" | "sse";
|
||||
export type Transport = "stdio" | "http" | "sse" | "sdk";
|
||||
|
||||
export type CodexConfigValue = string | number | boolean | CodexConfigValue[] | CodexConfigObject;
|
||||
export type CodexConfigObject = {
|
||||
[key: string]: CodexConfigValue;
|
||||
};
|
||||
|
||||
export type ClaudeMcpServer = McpServerConfigForProcessTransport | McpServerConfig;
|
||||
|
||||
export type SharedMcpServer = {
|
||||
type?: Transport;
|
||||
name?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
@@ -36,7 +43,7 @@ export type SharedMcpConfigFile = {
|
||||
mcp_servers?: Record<string, CodexConfigObject>;
|
||||
};
|
||||
claude?: {
|
||||
mcpServers?: Record<string, McpServerConfig>;
|
||||
mcpServers?: Record<string, ClaudeMcpServer>;
|
||||
};
|
||||
handlerSettings?: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
@@ -48,8 +55,164 @@ export type McpLoadContext = {
|
||||
|
||||
export type LoadedMcpConfig = {
|
||||
codexConfig?: NonNullable<CodexOptions["config"]>;
|
||||
claudeMcpServers?: Record<string, McpServerConfig>;
|
||||
claudeMcpServers?: Record<string, ClaudeMcpServer>;
|
||||
sourcePath?: string;
|
||||
resolvedHandlers?: Record<string, string>;
|
||||
};
|
||||
|
||||
const nonEmptyStringSchema = z.string().trim().min(1);
|
||||
const nonNegativeFiniteNumberSchema = z.number().finite().min(0);
|
||||
const stringRecordSchema = z.record(z.string().min(1), nonEmptyStringSchema);
|
||||
const stringArraySchema = z.array(nonEmptyStringSchema);
|
||||
|
||||
function isHttpLikeUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const httpLikeUrlSchema = nonEmptyStringSchema.refine(
|
||||
(value) => isHttpLikeUrl(value),
|
||||
'Expected an absolute "http" or "https" URL.',
|
||||
);
|
||||
|
||||
const codexConfigValueSchema: z.ZodType<CodexConfigValue> = z.lazy(() =>
|
||||
z.union([
|
||||
z.string(),
|
||||
z.number().finite(),
|
||||
z.boolean(),
|
||||
z.array(codexConfigValueSchema),
|
||||
z.record(z.string().min(1), codexConfigValueSchema),
|
||||
]),
|
||||
);
|
||||
|
||||
const codexConfigObjectSchema: z.ZodType<CodexConfigObject> = z.record(
|
||||
z.string().min(1),
|
||||
codexConfigValueSchema,
|
||||
);
|
||||
|
||||
export const sharedMcpServerSchema: z.ZodType<SharedMcpServer> = z
|
||||
.object({
|
||||
type: z.enum(["stdio", "http", "sse", "sdk"]).optional(),
|
||||
name: nonEmptyStringSchema.optional(),
|
||||
command: nonEmptyStringSchema.optional(),
|
||||
args: stringArraySchema.optional(),
|
||||
env: stringRecordSchema.optional(),
|
||||
cwd: nonEmptyStringSchema.optional(),
|
||||
url: httpLikeUrlSchema.optional(),
|
||||
headers: stringRecordSchema.optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
required: z.boolean().optional(),
|
||||
enabled_tools: stringArraySchema.optional(),
|
||||
disabled_tools: stringArraySchema.optional(),
|
||||
startup_timeout_sec: nonNegativeFiniteNumberSchema.optional(),
|
||||
tool_timeout_sec: nonNegativeFiniteNumberSchema.optional(),
|
||||
bearer_token_env_var: nonEmptyStringSchema.optional(),
|
||||
http_headers: stringRecordSchema.optional(),
|
||||
env_http_headers: stringRecordSchema.optional(),
|
||||
env_vars: stringArraySchema.optional(),
|
||||
handler: nonEmptyStringSchema.optional(),
|
||||
handlerOptions: z.record(z.string().min(1), z.unknown()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((server, context) => {
|
||||
const inferredType = server.type ?? (server.url ? "http" : "stdio");
|
||||
|
||||
if (inferredType === "stdio" && !server.command) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["command"],
|
||||
message: '"command" is required when MCP transport is "stdio".',
|
||||
});
|
||||
}
|
||||
|
||||
if ((inferredType === "http" || inferredType === "sse") && !server.url) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["url"],
|
||||
message: `"url" is required when MCP transport is "${inferredType}".`,
|
||||
});
|
||||
}
|
||||
|
||||
if (inferredType === "sdk" && !server.name) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["name"],
|
||||
message: '"name" is required when MCP transport is "sdk".',
|
||||
});
|
||||
}
|
||||
|
||||
if ((inferredType === "http" || inferredType === "sse") && server.command) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["command"],
|
||||
message: `"command" is only valid for "stdio" transport.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (inferredType === "sdk" && (server.command || server.url)) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: '"sdk" transport cannot declare "command" or "url".',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const claudeMcpServerSchema: z.ZodType<McpServerConfigForProcessTransport> = z.union([
|
||||
z
|
||||
.object({
|
||||
type: z.literal("stdio"),
|
||||
command: nonEmptyStringSchema,
|
||||
args: stringArraySchema.optional(),
|
||||
env: stringRecordSchema.optional(),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("http"),
|
||||
url: httpLikeUrlSchema,
|
||||
headers: stringRecordSchema.optional(),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("sse"),
|
||||
url: httpLikeUrlSchema,
|
||||
headers: stringRecordSchema.optional(),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("sdk"),
|
||||
name: nonEmptyStringSchema,
|
||||
})
|
||||
.strict(),
|
||||
]);
|
||||
|
||||
export const McpConfigSchema: z.ZodType<SharedMcpConfigFile> = z
|
||||
.object({
|
||||
servers: z.record(z.string().min(1), sharedMcpServerSchema).optional(),
|
||||
codex: z
|
||||
.object({
|
||||
mcp_servers: z.record(z.string().min(1), codexConfigObjectSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
claude: z
|
||||
.object({
|
||||
mcpServers: z.record(z.string().min(1), claudeMcpServerSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
handlerSettings: z
|
||||
.record(z.string().min(1), z.record(z.string().min(1), z.unknown()))
|
||||
.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export function parseMcpConfig(input: unknown): SharedMcpConfigFile {
|
||||
return McpConfigSchema.parse(input);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user