Generalizes the v1.14.1 single-server Context7 PoC into a multi-server MCP client registry with per-server graceful degradation. JSON config at /data/mcp.json (bind-mounted alongside AGENTS.md) matches opencode's mcpServers schema shape. Config file missing = no MCP (opt-in by presence). Two transports: Streamable HTTP (remote servers like Context7) and stdio (local subprocess servers like codecontext). Stdio spawns a persistent child via the SDK's StdioClientTransport; shutdown hook closes all transports. Tool prefix generalized from context7_<name> to <serverName>_<toolName> with a toolToServer reverse map for dispatch routing. AGENTS.md tools: field now supports glob patterns (context7_*, !web_*) via matchToolGlob — last-match- wins with ! deny prefix. Replaces exact-match .includes() in stream-phase.ts. refreshToolNames() in agents.ts rebuilds the DEFAULT_TOOLS snapshot after appendMcpTools so agents without explicit tools: lists see MCP tools — reviewer caught that the module-load-time snapshot would permanently exclude late-registered tools. Read-only invariant: readOnlyHint === false rejected at discovery. Result size capped at 5MB. v1.14.1 env vars removed — superseded by config file. Default data/mcp.json ships with Context7 disabled. 363/363 server tests passing. No schema changes, no frontend changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
/**
|
|
* v1.15.0-mcp-multi: MCP config file schema + loader.
|
|
*
|
|
* Reads a JSON config file (default `/data/mcp.json`) that declares MCP
|
|
* servers — their transport type, connection parameters, and enabled state.
|
|
* Schema shape matches opencode's `mcpServers` key for copy-paste compat.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { z } from 'zod';
|
|
import type { FastifyBaseLogger } from 'fastify';
|
|
|
|
// ---- Zod schema ----
|
|
|
|
const McpServerConfigSchema = z.discriminatedUnion('type', [
|
|
z.object({
|
|
type: z.literal('streamableHttp'),
|
|
url: z.string().url(),
|
|
headers: z.record(z.string()).optional(),
|
|
enabled: z.boolean().default(true),
|
|
}),
|
|
z.object({
|
|
type: z.literal('stdio'),
|
|
command: z.string().min(1),
|
|
args: z.array(z.string()).default([]),
|
|
env: z.record(z.string()).optional(),
|
|
enabled: z.boolean().default(true),
|
|
}),
|
|
]);
|
|
|
|
const McpConfigSchema = z.object({
|
|
mcpServers: z.record(z.string(), McpServerConfigSchema).default({}),
|
|
});
|
|
|
|
export type McpServerConfig = z.infer<typeof McpServerConfigSchema>;
|
|
|
|
export interface McpServerEntry {
|
|
name: string;
|
|
config: McpServerConfig;
|
|
}
|
|
|
|
// ---- Loader ----
|
|
|
|
/**
|
|
* Read and validate the MCP config file. Returns enabled servers only.
|
|
* File missing → log info, return []. Parse/validation error → log warn, return [].
|
|
*/
|
|
export function loadMcpConfig(configPath: string, log: FastifyBaseLogger): McpServerEntry[] {
|
|
let raw: string;
|
|
try {
|
|
raw = readFileSync(configPath, 'utf8');
|
|
} catch {
|
|
log.info(`mcp: config not found at ${configPath}, skipping`);
|
|
return [];
|
|
}
|
|
|
|
let json: unknown;
|
|
try {
|
|
json = JSON.parse(raw);
|
|
} catch (err) {
|
|
log.warn({ err }, `mcp: failed to parse ${configPath} as JSON`);
|
|
return [];
|
|
}
|
|
|
|
const result = McpConfigSchema.safeParse(json);
|
|
if (!result.success) {
|
|
log.warn({ errors: result.error.flatten().fieldErrors }, `mcp: invalid config at ${configPath}`);
|
|
return [];
|
|
}
|
|
|
|
const entries: McpServerEntry[] = [];
|
|
for (const [name, config] of Object.entries(result.data.mcpServers)) {
|
|
if (config.enabled) {
|
|
entries.push({ name, config });
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|