Files
Codeman/src/web/routes/mcp-sync-routes.ts
T
DevvynandClaude Sonnet 5.5 e6b258fc44 feat(mcp): sync MCP servers across enabled CLIs
Adds capabilities.mcpConfig to the CLI registry (Claude, Gemini, Codex,
OpenCode), an additive src/mcp-sync.ts, GET/POST /api/mcp-sync and a
Settings > Agents & CLIs control. Never edits or removes an existing
server; backs up each file it changes; reports conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 18:27:06 +08:00

46 lines
1.9 KiB
TypeScript

/**
* @fileoverview MCP server sync (src/mcp-sync.ts).
*
* GET /api/mcp-sync — dry run: per enabled CLI, which servers it has and which it would gain.
* POST /api/mcp-sync — apply: add the missing servers to each CLI's own config file.
*
* Writes files in the SERVER user's home, so in multi-user mode it is admin only. Responses
* carry server names only, never env values or headers.
*/
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { ApiErrorCode, createErrorResponse, getErrorMessage, type ApiResponse } from '../../types.js';
import { isAdmin } from '../route-helpers.js';
import { isMultiUserMode } from '../../config/multiuser.js';
import { enabledClis } from '../../config/cli-registry/registry.js';
import { syncMcpServers, type McpSyncResult, type McpSyncTarget } from '../../mcp-sync.js';
/** Enabled CLIs that declare an MCP config file, in registry order (first definition wins). */
export function mcpSyncTargets(): McpSyncTarget[] {
return enabledClis()
.filter((e) => e.capabilities.mcpConfig)
.sort((a, b) => a.order - b.order)
.map((e) => ({ id: e.id, label: e.label, ...e.capabilities.mcpConfig! }));
}
function gate(req: FastifyRequest): ApiResponse<never> | null {
if (isMultiUserMode() && !isAdmin(req)) {
return createErrorResponse(ApiErrorCode.FORBIDDEN, 'Admin only in multi-user mode');
}
return null;
}
export function registerMcpSyncRoutes(app: FastifyInstance): void {
const run = async (req: FastifyRequest, apply: boolean): Promise<ApiResponse<McpSyncResult>> => {
const denied = gate(req);
if (denied) return denied;
try {
return { success: true, data: await syncMcpServers(mcpSyncTargets(), { apply }) };
} catch (err) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
}
};
app.get('/api/mcp-sync', (req) => run(req, false));
app.post('/api/mcp-sync', (req) => run(req, true));
}