mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
Remove incorrect path separator conversion that caused directory creation issues on Linux/WSL platforms. The function was converting forward slashes to backslashes, which are treated as literal filename characters on Unix systems rather than path separators. Changes: - Remove manual path normalization in getProjectSettingsPath() - Rely on Node.js path.join() for cross-platform compatibility - Fix affects both hooks-routes.ts and mcp-routes.ts Impact: - Linux/WSL: Fixes incorrect directory creation - Windows: No behavior change, maintains correct functionality Fixes project-level hook settings being saved to wrong location when using Dashboard frontend on Linux/WSL systems.
1272 lines
40 KiB
TypeScript
1272 lines
40 KiB
TypeScript
// @ts-nocheck
|
|
/**
|
|
* MCP Routes Module
|
|
* Handles all MCP-related API endpoints
|
|
*/
|
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
import { join, dirname } from 'path';
|
|
import { homedir } from 'os';
|
|
import * as McpTemplatesDb from './mcp-templates-db.js';
|
|
|
|
// Claude config file path
|
|
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
|
|
|
// Codex config file path (TOML format)
|
|
const CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
|
|
|
|
// Workspace root path for scanning .mcp.json files
|
|
let WORKSPACE_ROOT = process.cwd();
|
|
|
|
// ========================================
|
|
// TOML Parser for Codex Config
|
|
// ========================================
|
|
|
|
/**
|
|
* Simple TOML parser for Codex config.toml
|
|
* Supports basic types: strings, numbers, booleans, arrays, inline tables
|
|
*/
|
|
function parseToml(content: string): Record<string, any> {
|
|
const result: Record<string, any> = {};
|
|
let currentSection: string[] = [];
|
|
const lines = content.split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
let line = lines[i].trim();
|
|
|
|
// Skip empty lines and comments
|
|
if (!line || line.startsWith('#')) continue;
|
|
|
|
// Handle section headers [section] or [section.subsection]
|
|
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
if (sectionMatch) {
|
|
currentSection = sectionMatch[1].split('.');
|
|
// Ensure nested sections exist
|
|
let obj = result;
|
|
for (const part of currentSection) {
|
|
if (!obj[part]) obj[part] = {};
|
|
obj = obj[part];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Handle key = value pairs
|
|
const keyValueMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
|
|
if (keyValueMatch) {
|
|
const key = keyValueMatch[1];
|
|
const rawValue = keyValueMatch[2].trim();
|
|
const value = parseTomlValue(rawValue);
|
|
|
|
// Navigate to current section
|
|
let obj = result;
|
|
for (const part of currentSection) {
|
|
if (!obj[part]) obj[part] = {};
|
|
obj = obj[part];
|
|
}
|
|
obj[key] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parse a TOML value
|
|
*/
|
|
function parseTomlValue(value: string): any {
|
|
// String (double-quoted)
|
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
}
|
|
|
|
// String (single-quoted - literal)
|
|
if (value.startsWith("'") && value.endsWith("'")) {
|
|
return value.slice(1, -1);
|
|
}
|
|
|
|
// Boolean
|
|
if (value === 'true') return true;
|
|
if (value === 'false') return false;
|
|
|
|
// Number
|
|
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
return value.includes('.') ? parseFloat(value) : parseInt(value, 10);
|
|
}
|
|
|
|
// Array
|
|
if (value.startsWith('[') && value.endsWith(']')) {
|
|
const inner = value.slice(1, -1).trim();
|
|
if (!inner) return [];
|
|
// Simple array parsing (handles basic cases)
|
|
const items: any[] = [];
|
|
let depth = 0;
|
|
let current = '';
|
|
let inString = false;
|
|
let stringChar = '';
|
|
|
|
for (const char of inner) {
|
|
if (!inString && (char === '"' || char === "'")) {
|
|
inString = true;
|
|
stringChar = char;
|
|
current += char;
|
|
} else if (inString && char === stringChar) {
|
|
inString = false;
|
|
current += char;
|
|
} else if (!inString && (char === '[' || char === '{')) {
|
|
depth++;
|
|
current += char;
|
|
} else if (!inString && (char === ']' || char === '}')) {
|
|
depth--;
|
|
current += char;
|
|
} else if (!inString && char === ',' && depth === 0) {
|
|
items.push(parseTomlValue(current.trim()));
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
if (current.trim()) {
|
|
items.push(parseTomlValue(current.trim()));
|
|
}
|
|
return items;
|
|
}
|
|
|
|
// Inline table { key = value, ... }
|
|
if (value.startsWith('{') && value.endsWith('}')) {
|
|
const inner = value.slice(1, -1).trim();
|
|
if (!inner) return {};
|
|
const table: Record<string, any> = {};
|
|
// Simple inline table parsing
|
|
const pairs = inner.split(',');
|
|
for (const pair of pairs) {
|
|
const match = pair.trim().match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
|
|
if (match) {
|
|
table[match[1]] = parseTomlValue(match[2].trim());
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
|
|
// Return as string if nothing else matches
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Serialize object to TOML format for Codex config
|
|
*
|
|
* Handles mixed objects containing both simple values and sub-objects.
|
|
* For example: { command: "cmd", args: [...], env: { KEY: "value" } }
|
|
* becomes:
|
|
* [section]
|
|
* command = "cmd"
|
|
* args = [...]
|
|
* [section.env]
|
|
* KEY = "value"
|
|
*/
|
|
function serializeToml(obj: Record<string, any>, prefix: string = ''): string {
|
|
let result = '';
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (value === null || value === undefined) continue;
|
|
|
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
// Handle nested sections (like mcp_servers.server_name)
|
|
const sectionKey = prefix ? `${prefix}.${key}` : key;
|
|
|
|
// Separate simple values from sub-objects
|
|
const simpleEntries: [string, any][] = [];
|
|
const objectEntries: [string, any][] = [];
|
|
|
|
for (const [subKey, subValue] of Object.entries(value)) {
|
|
if (subValue === null || subValue === undefined) continue;
|
|
if (typeof subValue === 'object' && !Array.isArray(subValue)) {
|
|
objectEntries.push([subKey, subValue]);
|
|
} else {
|
|
simpleEntries.push([subKey, subValue]);
|
|
}
|
|
}
|
|
|
|
// Write section header if there are simple values
|
|
if (simpleEntries.length > 0) {
|
|
result += `\n[${sectionKey}]\n`;
|
|
for (const [subKey, subValue] of simpleEntries) {
|
|
result += `${subKey} = ${serializeTomlValue(subValue)}\n`;
|
|
}
|
|
}
|
|
|
|
// Recursively handle sub-objects
|
|
if (objectEntries.length > 0) {
|
|
for (const [subKey, subValue] of objectEntries) {
|
|
const subSectionKey = `${sectionKey}.${subKey}`;
|
|
|
|
// Check if sub-object has nested objects
|
|
const hasNestedObjects = Object.values(subValue).some(
|
|
v => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
);
|
|
|
|
if (hasNestedObjects) {
|
|
// Recursively process nested objects
|
|
result += serializeToml({ [subKey]: subValue }, sectionKey);
|
|
} else {
|
|
// Write sub-section with simple values
|
|
result += `\n[${subSectionKey}]\n`;
|
|
for (const [nestedKey, nestedValue] of Object.entries(subValue)) {
|
|
if (nestedValue !== null && nestedValue !== undefined) {
|
|
result += `${nestedKey} = ${serializeTomlValue(nestedValue)}\n`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no simple values but has object entries, still need to process
|
|
if (simpleEntries.length === 0 && objectEntries.length === 0) {
|
|
// Empty section - write header only
|
|
result += `\n[${sectionKey}]\n`;
|
|
}
|
|
} else if (!prefix) {
|
|
// Top-level simple values
|
|
result += `${key} = ${serializeTomlValue(value)}\n`;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Serialize a value to TOML format
|
|
*/
|
|
function serializeTomlValue(value: any): string {
|
|
if (typeof value === 'string') {
|
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
}
|
|
if (typeof value === 'boolean') {
|
|
return value ? 'true' : 'false';
|
|
}
|
|
if (typeof value === 'number') {
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return `[${value.map(v => serializeTomlValue(v)).join(', ')}]`;
|
|
}
|
|
if (typeof value === 'object' && value !== null) {
|
|
const pairs = Object.entries(value)
|
|
.filter(([_, v]) => v !== null && v !== undefined)
|
|
.map(([k, v]) => `${k} = ${serializeTomlValue(v)}`);
|
|
return `{ ${pairs.join(', ')} }`;
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
// ========================================
|
|
// Codex MCP Functions
|
|
// ========================================
|
|
|
|
/**
|
|
* Read Codex config.toml and extract MCP servers
|
|
*/
|
|
function getCodexMcpConfig(): { servers: Record<string, any>; configPath: string; exists: boolean } {
|
|
try {
|
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
return { servers: {}, configPath: CODEX_CONFIG_PATH, exists: false };
|
|
}
|
|
|
|
const content = readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
const config = parseToml(content);
|
|
|
|
// MCP servers are under [mcp_servers] section
|
|
const mcpServers = config.mcp_servers || {};
|
|
|
|
return {
|
|
servers: mcpServers,
|
|
configPath: CODEX_CONFIG_PATH,
|
|
exists: true
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error reading Codex config:', error);
|
|
return { servers: {}, configPath: CODEX_CONFIG_PATH, exists: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add or update MCP server in Codex config.toml
|
|
*/
|
|
function addCodexMcpServer(serverName: string, serverConfig: Record<string, any>): { success?: boolean; error?: string } {
|
|
try {
|
|
const codexDir = join(homedir(), '.codex');
|
|
|
|
// Ensure .codex directory exists
|
|
if (!existsSync(codexDir)) {
|
|
mkdirSync(codexDir, { recursive: true });
|
|
}
|
|
|
|
let config: Record<string, any> = {};
|
|
|
|
// Read existing config if it exists
|
|
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
const content = readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
config = parseToml(content);
|
|
}
|
|
|
|
// Ensure mcp_servers section exists
|
|
if (!config.mcp_servers) {
|
|
config.mcp_servers = {};
|
|
}
|
|
|
|
// Convert serverConfig from Claude format to Codex format
|
|
const codexServerConfig: Record<string, any> = {};
|
|
|
|
// Handle STDIO servers (command-based)
|
|
if (serverConfig.command) {
|
|
codexServerConfig.command = serverConfig.command;
|
|
if (serverConfig.args && serverConfig.args.length > 0) {
|
|
codexServerConfig.args = serverConfig.args;
|
|
}
|
|
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
|
codexServerConfig.env = serverConfig.env;
|
|
}
|
|
if (serverConfig.cwd) {
|
|
codexServerConfig.cwd = serverConfig.cwd;
|
|
}
|
|
}
|
|
|
|
// Handle HTTP servers (url-based)
|
|
if (serverConfig.url) {
|
|
codexServerConfig.url = serverConfig.url;
|
|
if (serverConfig.bearer_token_env_var) {
|
|
codexServerConfig.bearer_token_env_var = serverConfig.bearer_token_env_var;
|
|
}
|
|
if (serverConfig.http_headers) {
|
|
codexServerConfig.http_headers = serverConfig.http_headers;
|
|
}
|
|
}
|
|
|
|
// Copy optional fields
|
|
if (serverConfig.startup_timeout_sec !== undefined) {
|
|
codexServerConfig.startup_timeout_sec = serverConfig.startup_timeout_sec;
|
|
}
|
|
if (serverConfig.tool_timeout_sec !== undefined) {
|
|
codexServerConfig.tool_timeout_sec = serverConfig.tool_timeout_sec;
|
|
}
|
|
if (serverConfig.enabled !== undefined) {
|
|
codexServerConfig.enabled = serverConfig.enabled;
|
|
}
|
|
if (serverConfig.enabled_tools) {
|
|
codexServerConfig.enabled_tools = serverConfig.enabled_tools;
|
|
}
|
|
if (serverConfig.disabled_tools) {
|
|
codexServerConfig.disabled_tools = serverConfig.disabled_tools;
|
|
}
|
|
|
|
// Add the server
|
|
config.mcp_servers[serverName] = codexServerConfig;
|
|
|
|
// Serialize and write back
|
|
const tomlContent = serializeToml(config);
|
|
writeFileSync(CODEX_CONFIG_PATH, tomlContent, 'utf8');
|
|
|
|
return { success: true };
|
|
} catch (error: unknown) {
|
|
console.error('Error adding Codex MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove MCP server from Codex config.toml
|
|
*/
|
|
function removeCodexMcpServer(serverName: string): { success?: boolean; error?: string } {
|
|
try {
|
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
return { error: 'Codex config.toml not found' };
|
|
}
|
|
|
|
const content = readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
const config = parseToml(content);
|
|
|
|
if (!config.mcp_servers || !config.mcp_servers[serverName]) {
|
|
return { error: `Server not found: ${serverName}` };
|
|
}
|
|
|
|
// Remove the server
|
|
delete config.mcp_servers[serverName];
|
|
|
|
// Serialize and write back
|
|
const tomlContent = serializeToml(config);
|
|
writeFileSync(CODEX_CONFIG_PATH, tomlContent, 'utf8');
|
|
|
|
return { success: true };
|
|
} catch (error: unknown) {
|
|
console.error('Error removing Codex MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle Codex MCP server enabled state
|
|
*/
|
|
function toggleCodexMcpServer(serverName: string, enabled: boolean): { success?: boolean; error?: string } {
|
|
try {
|
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
return { error: 'Codex config.toml not found' };
|
|
}
|
|
|
|
const content = readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
const config = parseToml(content);
|
|
|
|
if (!config.mcp_servers || !config.mcp_servers[serverName]) {
|
|
return { error: `Server not found: ${serverName}` };
|
|
}
|
|
|
|
// Set enabled state
|
|
config.mcp_servers[serverName].enabled = enabled;
|
|
|
|
// Serialize and write back
|
|
const tomlContent = serializeToml(config);
|
|
writeFileSync(CODEX_CONFIG_PATH, tomlContent, 'utf8');
|
|
|
|
return { success: true };
|
|
} catch (error: unknown) {
|
|
console.error('Error toggling Codex MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
export interface RouteContext {
|
|
pathname: string;
|
|
url: URL;
|
|
req: IncomingMessage;
|
|
res: ServerResponse;
|
|
initialPath: string;
|
|
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
|
broadcastToClients: (data: unknown) => void;
|
|
}
|
|
|
|
// ========================================
|
|
// Helper Functions
|
|
// ========================================
|
|
|
|
/**
|
|
* Get enterprise managed MCP path (platform-specific)
|
|
*/
|
|
function getEnterpriseMcpPath(): string {
|
|
const platform = process.platform;
|
|
if (platform === 'darwin') {
|
|
return '/Library/Application Support/ClaudeCode/managed-mcp.json';
|
|
} else if (platform === 'win32') {
|
|
return 'C:\\Program Files\\ClaudeCode\\managed-mcp.json';
|
|
} else {
|
|
// Linux and WSL
|
|
return '/etc/claude-code/managed-mcp.json';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely read and parse JSON file
|
|
*/
|
|
function safeReadJson(filePath) {
|
|
try {
|
|
if (!existsSync(filePath)) return null;
|
|
const content = readFileSync(filePath, 'utf8');
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get MCP servers from a JSON file (expects mcpServers key at top level)
|
|
* @param {string} filePath
|
|
* @returns {Object} mcpServers object or empty object
|
|
*/
|
|
function getMcpServersFromFile(filePath) {
|
|
const config = safeReadJson(filePath);
|
|
if (!config) return {};
|
|
return config.mcpServers || {};
|
|
}
|
|
|
|
/**
|
|
* Add or update MCP server in project's .mcp.json file
|
|
* @param {string} projectPath - Project directory path
|
|
* @param {string} serverName - MCP server name
|
|
* @param {Object} serverConfig - MCP server configuration
|
|
* @returns {Object} Result with success/error
|
|
*/
|
|
function addMcpServerToMcpJson(projectPath, serverName, serverConfig) {
|
|
try {
|
|
const normalizedPath = normalizePathForFileSystem(projectPath);
|
|
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
|
|
|
// Read existing .mcp.json or create new structure
|
|
let mcpJson = safeReadJson(mcpJsonPath) || { mcpServers: {} };
|
|
|
|
// Ensure mcpServers exists
|
|
if (!mcpJson.mcpServers) {
|
|
mcpJson.mcpServers = {};
|
|
}
|
|
|
|
// Add or update the server
|
|
mcpJson.mcpServers[serverName] = serverConfig;
|
|
|
|
// Write back to .mcp.json
|
|
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2), 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
serverConfig,
|
|
scope: 'project-mcp-json',
|
|
path: mcpJsonPath
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error adding MCP server to .mcp.json:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove MCP server from project's .mcp.json file
|
|
* @param {string} projectPath - Project directory path
|
|
* @param {string} serverName - MCP server name
|
|
* @returns {Object} Result with success/error
|
|
*/
|
|
function removeMcpServerFromMcpJson(projectPath, serverName) {
|
|
try {
|
|
const normalizedPath = normalizePathForFileSystem(projectPath);
|
|
const mcpJsonPath = join(normalizedPath, '.mcp.json');
|
|
|
|
if (!existsSync(mcpJsonPath)) {
|
|
return { error: '.mcp.json not found' };
|
|
}
|
|
|
|
const mcpJson = safeReadJson(mcpJsonPath);
|
|
if (!mcpJson || !mcpJson.mcpServers || !mcpJson.mcpServers[serverName]) {
|
|
return { error: `Server not found: ${serverName}` };
|
|
}
|
|
|
|
// Remove the server
|
|
delete mcpJson.mcpServers[serverName];
|
|
|
|
// Write back to .mcp.json
|
|
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2), 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
removed: true,
|
|
scope: 'project-mcp-json'
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error removing MCP server from .mcp.json:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get MCP configuration from multiple sources (per official Claude Code docs):
|
|
*
|
|
* Priority (highest to lowest):
|
|
* 1. Enterprise managed-mcp.json (cannot be overridden)
|
|
* 2. Local scope (project-specific private in ~/.claude.json)
|
|
* 3. Project scope (.mcp.json in project root)
|
|
* 4. User scope (mcpServers in ~/.claude.json)
|
|
*
|
|
* Note: ~/.claude/settings.json is for MCP PERMISSIONS, NOT definitions!
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
function getMcpConfig() {
|
|
try {
|
|
const result = {
|
|
projects: {},
|
|
userServers: {}, // User-level servers from ~/.claude.json mcpServers
|
|
enterpriseServers: {}, // Enterprise managed servers (highest priority)
|
|
configSources: [] // Track where configs came from for debugging
|
|
};
|
|
|
|
// 1. Read Enterprise managed MCP servers (highest priority)
|
|
const enterprisePath = getEnterpriseMcpPath();
|
|
if (existsSync(enterprisePath)) {
|
|
const enterpriseConfig = safeReadJson(enterprisePath);
|
|
if (enterpriseConfig?.mcpServers) {
|
|
result.enterpriseServers = enterpriseConfig.mcpServers;
|
|
result.configSources.push({ type: 'enterprise', path: enterprisePath, count: Object.keys(enterpriseConfig.mcpServers).length });
|
|
}
|
|
}
|
|
|
|
// 2. Read from ~/.claude.json
|
|
if (existsSync(CLAUDE_CONFIG_PATH)) {
|
|
const claudeConfig = safeReadJson(CLAUDE_CONFIG_PATH);
|
|
if (claudeConfig) {
|
|
// 2a. User-level mcpServers (top-level mcpServers key)
|
|
if (claudeConfig.mcpServers) {
|
|
result.userServers = claudeConfig.mcpServers;
|
|
result.configSources.push({ type: 'user', path: CLAUDE_CONFIG_PATH, count: Object.keys(claudeConfig.mcpServers).length });
|
|
}
|
|
|
|
// 2b. Project-specific configurations (projects[path].mcpServers)
|
|
if (claudeConfig.projects) {
|
|
result.projects = claudeConfig.projects;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. For each known project, check for .mcp.json (project-level config)
|
|
// .mcp.json is now the PRIMARY source for project-level MCP servers
|
|
const projectPaths = Object.keys(result.projects);
|
|
for (const projectPath of projectPaths) {
|
|
const mcpJsonPath = join(projectPath, '.mcp.json');
|
|
if (existsSync(mcpJsonPath)) {
|
|
const mcpJsonConfig = safeReadJson(mcpJsonPath);
|
|
if (mcpJsonConfig?.mcpServers) {
|
|
// Merge .mcp.json servers into project config
|
|
// .mcp.json has HIGHER priority than ~/.claude.json projects[path].mcpServers
|
|
const existingServers = result.projects[projectPath]?.mcpServers || {};
|
|
result.projects[projectPath] = {
|
|
...result.projects[projectPath],
|
|
mcpServers: {
|
|
...existingServers, // ~/.claude.json projects[path] (lower priority, legacy)
|
|
...mcpJsonConfig.mcpServers // .mcp.json (higher priority, new default)
|
|
},
|
|
mcpJsonPath: mcpJsonPath, // Track source for debugging
|
|
hasMcpJson: true
|
|
};
|
|
result.configSources.push({
|
|
type: 'project-mcp-json',
|
|
path: mcpJsonPath,
|
|
count: Object.keys(mcpJsonConfig.mcpServers).length
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build globalServers by merging user and enterprise servers
|
|
// Enterprise servers override user servers
|
|
result.globalServers = {
|
|
...result.userServers,
|
|
...result.enterpriseServers
|
|
};
|
|
|
|
return result;
|
|
} catch (error: unknown) {
|
|
console.error('Error reading MCP config:', error);
|
|
return { projects: {}, globalServers: {}, userServers: {}, enterpriseServers: {}, configSources: [], error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalize path to filesystem format (for accessing .mcp.json files)
|
|
* Always uses forward slashes for cross-platform compatibility
|
|
* @param {string} path
|
|
* @returns {string}
|
|
*/
|
|
function normalizePathForFileSystem(path) {
|
|
let normalized = path.replace(/\\/g, '/');
|
|
|
|
// Handle /d/path format -> D:/path
|
|
if (normalized.match(/^\/[a-zA-Z]\//)) {
|
|
normalized = normalized.charAt(1).toUpperCase() + ':' + normalized.slice(2);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Normalize project path to match existing format in .claude.json
|
|
* Checks both forward slash and backslash formats to find existing entry
|
|
* @param {string} path
|
|
* @param {Object} claudeConfig - Optional existing config to check format
|
|
* @returns {string}
|
|
*/
|
|
function normalizeProjectPathForConfig(path, claudeConfig = null) {
|
|
// IMPORTANT: Always normalize to forward slashes to prevent duplicate entries
|
|
// (e.g., prevents both "D:/Claude_dms3" and "D:\\Claude_dms3")
|
|
let normalizedForward = path.replace(/\\/g, '/');
|
|
|
|
// Handle /d/path format -> D:/path
|
|
if (normalizedForward.match(/^\/[a-zA-Z]\//)) {
|
|
normalizedForward = normalizedForward.charAt(1).toUpperCase() + ':' + normalizedForward.slice(2);
|
|
}
|
|
|
|
// ALWAYS return forward slash format to prevent duplicates
|
|
return normalizedForward;
|
|
}
|
|
|
|
/**
|
|
* Toggle MCP server enabled/disabled
|
|
* @param {string} projectPath
|
|
* @param {string} serverName
|
|
* @param {boolean} enable
|
|
* @returns {Object}
|
|
*/
|
|
function toggleMcpServerEnabled(projectPath, serverName, enable) {
|
|
try {
|
|
if (!existsSync(CLAUDE_CONFIG_PATH)) {
|
|
return { error: '.claude.json not found' };
|
|
}
|
|
|
|
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
|
const config = JSON.parse(content);
|
|
|
|
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
|
|
|
if (!config.projects || !config.projects[normalizedPath]) {
|
|
return { error: `Project not found: ${normalizedPath}` };
|
|
}
|
|
|
|
const projectConfig = config.projects[normalizedPath];
|
|
|
|
// Ensure disabledMcpServers array exists
|
|
if (!projectConfig.disabledMcpServers) {
|
|
projectConfig.disabledMcpServers = [];
|
|
}
|
|
|
|
if (enable) {
|
|
// Remove from disabled list
|
|
projectConfig.disabledMcpServers = projectConfig.disabledMcpServers.filter(s => s !== serverName);
|
|
} else {
|
|
// Add to disabled list if not already there
|
|
if (!projectConfig.disabledMcpServers.includes(serverName)) {
|
|
projectConfig.disabledMcpServers.push(serverName);
|
|
}
|
|
}
|
|
|
|
// Write back to file
|
|
writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
enabled: enable,
|
|
disabledMcpServers: projectConfig.disabledMcpServers
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error toggling MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add MCP server to project
|
|
* Now defaults to using .mcp.json instead of .claude.json
|
|
* @param {string} projectPath
|
|
* @param {string} serverName
|
|
* @param {Object} serverConfig
|
|
* @param {boolean} useLegacyConfig - If true, use .claude.json instead of .mcp.json
|
|
* @returns {Object}
|
|
*/
|
|
function addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyConfig = false) {
|
|
try {
|
|
// Default: Use .mcp.json for project-level MCP servers
|
|
if (!useLegacyConfig) {
|
|
return addMcpServerToMcpJson(projectPath, serverName, serverConfig);
|
|
}
|
|
|
|
// Legacy: Use .claude.json (kept for backward compatibility)
|
|
if (!existsSync(CLAUDE_CONFIG_PATH)) {
|
|
return { error: '.claude.json not found' };
|
|
}
|
|
|
|
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
|
const config = JSON.parse(content);
|
|
|
|
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
|
|
|
// Create project entry if it doesn't exist
|
|
if (!config.projects) {
|
|
config.projects = {};
|
|
}
|
|
|
|
if (!config.projects[normalizedPath]) {
|
|
config.projects[normalizedPath] = {
|
|
allowedTools: [],
|
|
mcpContextUris: [],
|
|
mcpServers: {},
|
|
enabledMcpjsonServers: [],
|
|
disabledMcpjsonServers: [],
|
|
hasTrustDialogAccepted: false,
|
|
projectOnboardingSeenCount: 0,
|
|
hasClaudeMdExternalIncludesApproved: false,
|
|
hasClaudeMdExternalIncludesWarningShown: false
|
|
};
|
|
}
|
|
|
|
const projectConfig = config.projects[normalizedPath];
|
|
|
|
// Ensure mcpServers exists
|
|
if (!projectConfig.mcpServers) {
|
|
projectConfig.mcpServers = {};
|
|
}
|
|
|
|
// Add the server
|
|
projectConfig.mcpServers[serverName] = serverConfig;
|
|
|
|
// Write back to file
|
|
writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
serverConfig,
|
|
scope: 'project-legacy'
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error adding MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove MCP server from project
|
|
* Checks both .mcp.json and .claude.json
|
|
* @param {string} projectPath
|
|
* @param {string} serverName
|
|
* @returns {Object}
|
|
*/
|
|
function removeMcpServerFromProject(projectPath, serverName) {
|
|
try {
|
|
const normalizedPathForFile = normalizePathForFileSystem(projectPath);
|
|
const mcpJsonPath = join(normalizedPathForFile, '.mcp.json');
|
|
|
|
let removedFromMcpJson = false;
|
|
let removedFromClaudeJson = false;
|
|
|
|
// Try to remove from .mcp.json first (new default)
|
|
if (existsSync(mcpJsonPath)) {
|
|
const mcpJson = safeReadJson(mcpJsonPath);
|
|
if (mcpJson?.mcpServers?.[serverName]) {
|
|
const result = removeMcpServerFromMcpJson(projectPath, serverName);
|
|
if (result.success) {
|
|
removedFromMcpJson = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also try to remove from .claude.json (legacy - may coexist)
|
|
if (existsSync(CLAUDE_CONFIG_PATH)) {
|
|
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
|
const config = JSON.parse(content);
|
|
|
|
// Get normalized path that matches existing config format
|
|
const normalizedPath = normalizeProjectPathForConfig(projectPath, config);
|
|
|
|
if (config.projects && config.projects[normalizedPath]) {
|
|
const projectConfig = config.projects[normalizedPath];
|
|
|
|
if (projectConfig.mcpServers && projectConfig.mcpServers[serverName]) {
|
|
// Remove the server
|
|
delete projectConfig.mcpServers[serverName];
|
|
|
|
// Also remove from disabled list if present
|
|
if (projectConfig.disabledMcpServers) {
|
|
projectConfig.disabledMcpServers = projectConfig.disabledMcpServers.filter(s => s !== serverName);
|
|
}
|
|
|
|
// Write back to file
|
|
writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
removedFromClaudeJson = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return success if removed from either location
|
|
if (removedFromMcpJson || removedFromClaudeJson) {
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
removed: true,
|
|
scope: removedFromMcpJson ? 'project-mcp-json' : 'project-legacy',
|
|
removedFrom: removedFromMcpJson && removedFromClaudeJson ? 'both' :
|
|
removedFromMcpJson ? '.mcp.json' : '.claude.json'
|
|
};
|
|
}
|
|
|
|
return { error: `Server not found: ${serverName}` };
|
|
} catch (error: unknown) {
|
|
console.error('Error removing MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add MCP server to global/user scope (top-level mcpServers in ~/.claude.json)
|
|
* @param {string} serverName
|
|
* @param {Object} serverConfig
|
|
* @returns {Object}
|
|
*/
|
|
function addGlobalMcpServer(serverName, serverConfig) {
|
|
try {
|
|
if (!existsSync(CLAUDE_CONFIG_PATH)) {
|
|
return { error: '.claude.json not found' };
|
|
}
|
|
|
|
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
|
const config = JSON.parse(content);
|
|
|
|
// Ensure top-level mcpServers exists
|
|
if (!config.mcpServers) {
|
|
config.mcpServers = {};
|
|
}
|
|
|
|
// Add the server to top-level mcpServers
|
|
config.mcpServers[serverName] = serverConfig;
|
|
|
|
// Write back to file
|
|
writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
serverConfig,
|
|
scope: 'global'
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error adding global MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove MCP server from global/user scope (top-level mcpServers)
|
|
* @param {string} serverName
|
|
* @returns {Object}
|
|
*/
|
|
function removeGlobalMcpServer(serverName) {
|
|
try {
|
|
if (!existsSync(CLAUDE_CONFIG_PATH)) {
|
|
return { error: '.claude.json not found' };
|
|
}
|
|
|
|
const content = readFileSync(CLAUDE_CONFIG_PATH, 'utf8');
|
|
const config = JSON.parse(content);
|
|
|
|
if (!config.mcpServers || !config.mcpServers[serverName]) {
|
|
return { error: `Global server not found: ${serverName}` };
|
|
}
|
|
|
|
// Remove the server from top-level mcpServers
|
|
delete config.mcpServers[serverName];
|
|
|
|
// Write back to file
|
|
writeFileSync(CLAUDE_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
|
|
return {
|
|
success: true,
|
|
serverName,
|
|
removed: true,
|
|
scope: 'global'
|
|
};
|
|
} catch (error: unknown) {
|
|
console.error('Error removing global MCP server:', error);
|
|
return { error: (error as Error).message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read settings file safely
|
|
* @param {string} filePath
|
|
* @returns {Object}
|
|
*/
|
|
function readSettingsFile(filePath) {
|
|
try {
|
|
if (!existsSync(filePath)) {
|
|
return {};
|
|
}
|
|
const content = readFileSync(filePath, 'utf8');
|
|
return JSON.parse(content);
|
|
} catch (error: unknown) {
|
|
console.error(`Error reading settings file ${filePath}:`, error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write settings file safely
|
|
* @param {string} filePath
|
|
* @param {Object} settings
|
|
*/
|
|
function writeSettingsFile(filePath, settings) {
|
|
const dirPath = dirname(filePath);
|
|
// Ensure directory exists
|
|
if (!existsSync(dirPath)) {
|
|
mkdirSync(dirPath, { recursive: true });
|
|
}
|
|
writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Get project settings path
|
|
* @param {string} projectPath
|
|
* @returns {string}
|
|
*/
|
|
function getProjectSettingsPath(projectPath) {
|
|
// path.join automatically handles cross-platform path separators
|
|
return join(projectPath, '.claude', 'settings.json');
|
|
}
|
|
|
|
// ========================================
|
|
// Route Handlers
|
|
// ========================================
|
|
|
|
/**
|
|
* Handle MCP routes
|
|
* @returns true if route was handled, false otherwise
|
|
*/
|
|
export async function handleMcpRoutes(ctx: RouteContext): Promise<boolean> {
|
|
const { pathname, url, req, res, initialPath, handlePostRequest, broadcastToClients } = ctx;
|
|
|
|
// API: Get MCP configuration (includes both Claude and Codex)
|
|
if (pathname === '/api/mcp-config') {
|
|
const mcpData = getMcpConfig();
|
|
const codexData = getCodexMcpConfig();
|
|
const combinedData = {
|
|
...mcpData,
|
|
codex: codexData
|
|
};
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(combinedData));
|
|
return true;
|
|
}
|
|
|
|
// ========================================
|
|
// Codex MCP API Endpoints
|
|
// ========================================
|
|
|
|
// API: Get Codex MCP configuration
|
|
if (pathname === '/api/codex-mcp-config') {
|
|
const codexData = getCodexMcpConfig();
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(codexData));
|
|
return true;
|
|
}
|
|
|
|
// API: Add Codex MCP server
|
|
if (pathname === '/api/codex-mcp-add' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { serverName, serverConfig } = body;
|
|
if (!serverName || !serverConfig) {
|
|
return { error: 'serverName and serverConfig are required', status: 400 };
|
|
}
|
|
return addCodexMcpServer(serverName, serverConfig);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Remove Codex MCP server
|
|
if (pathname === '/api/codex-mcp-remove' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { serverName } = body;
|
|
if (!serverName) {
|
|
return { error: 'serverName is required', status: 400 };
|
|
}
|
|
return removeCodexMcpServer(serverName);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Toggle Codex MCP server enabled state
|
|
if (pathname === '/api/codex-mcp-toggle' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { serverName, enabled } = body;
|
|
if (!serverName || enabled === undefined) {
|
|
return { error: 'serverName and enabled are required', status: 400 };
|
|
}
|
|
return toggleCodexMcpServer(serverName, enabled);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Toggle MCP server enabled/disabled
|
|
if (pathname === '/api/mcp-toggle' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { projectPath, serverName, enable } = body;
|
|
if (!projectPath || !serverName) {
|
|
return { error: 'projectPath and serverName are required', status: 400 };
|
|
}
|
|
return toggleMcpServerEnabled(projectPath, serverName, enable);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Copy MCP server to project
|
|
if (pathname === '/api/mcp-copy-server' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { projectPath, serverName, serverConfig, configType } = body;
|
|
if (!projectPath || !serverName || !serverConfig) {
|
|
return { error: 'projectPath, serverName, and serverConfig are required', status: 400 };
|
|
}
|
|
// configType: 'mcp' = use .mcp.json (default), 'claude' = use .claude.json
|
|
const useLegacyConfig = configType === 'claude';
|
|
return addMcpServerToProject(projectPath, serverName, serverConfig, useLegacyConfig);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Install CCW MCP server to project
|
|
if (pathname === '/api/mcp-install-ccw' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { projectPath } = body;
|
|
if (!projectPath) {
|
|
return { error: 'projectPath is required', status: 400 };
|
|
}
|
|
|
|
// Generate CCW MCP server config
|
|
// Use cmd /c to inherit Claude Code's working directory
|
|
const ccwMcpConfig = {
|
|
command: "cmd",
|
|
args: ["/c", "npx", "-y", "ccw-mcp"],
|
|
env: {
|
|
CCW_ENABLED_TOOLS: "all"
|
|
}
|
|
};
|
|
|
|
// Use existing addMcpServerToProject to install CCW MCP
|
|
return addMcpServerToProject(projectPath, 'ccw-tools', ccwMcpConfig);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Remove MCP server from project
|
|
if (pathname === '/api/mcp-remove-server' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { projectPath, serverName } = body;
|
|
if (!projectPath || !serverName) {
|
|
return { error: 'projectPath and serverName are required', status: 400 };
|
|
}
|
|
return removeMcpServerFromProject(projectPath, serverName);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Add MCP server to global scope (top-level mcpServers in ~/.claude.json)
|
|
if (pathname === '/api/mcp-add-global-server' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { serverName, serverConfig } = body;
|
|
if (!serverName || !serverConfig) {
|
|
return { error: 'serverName and serverConfig are required', status: 400 };
|
|
}
|
|
return addGlobalMcpServer(serverName, serverConfig);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Remove MCP server from global scope
|
|
if (pathname === '/api/mcp-remove-global-server' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { serverName } = body;
|
|
if (!serverName) {
|
|
return { error: 'serverName is required', status: 400 };
|
|
}
|
|
return removeGlobalMcpServer(serverName);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// ========================================
|
|
// MCP Templates API
|
|
// ========================================
|
|
|
|
// API: Get all MCP templates
|
|
if (pathname === '/api/mcp-templates' && req.method === 'GET') {
|
|
const templates = McpTemplatesDb.getAllTemplates();
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, templates }));
|
|
return true;
|
|
}
|
|
|
|
// API: Save MCP template
|
|
if (pathname === '/api/mcp-templates' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { name, description, serverConfig, tags, category } = body;
|
|
if (!name || !serverConfig) {
|
|
return { error: 'name and serverConfig are required', status: 400 };
|
|
}
|
|
return McpTemplatesDb.saveTemplate({
|
|
name,
|
|
description,
|
|
serverConfig,
|
|
tags,
|
|
category
|
|
});
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// API: Get template by name
|
|
if (pathname.startsWith('/api/mcp-templates/') && req.method === 'GET') {
|
|
const templateName = decodeURIComponent(pathname.split('/api/mcp-templates/')[1]);
|
|
const template = McpTemplatesDb.getTemplateByName(templateName);
|
|
if (template) {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, template }));
|
|
} else {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: false, error: 'Template not found' }));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// API: Delete MCP template
|
|
if (pathname.startsWith('/api/mcp-templates/') && req.method === 'DELETE') {
|
|
const templateName = decodeURIComponent(pathname.split('/api/mcp-templates/')[1]);
|
|
const result = McpTemplatesDb.deleteTemplate(templateName);
|
|
res.writeHead(result.success ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(result));
|
|
return true;
|
|
}
|
|
|
|
// API: Search MCP templates
|
|
if (pathname === '/api/mcp-templates/search' && req.method === 'GET') {
|
|
const keyword = url.searchParams.get('q') || '';
|
|
const templates = McpTemplatesDb.searchTemplates(keyword);
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, templates }));
|
|
return true;
|
|
}
|
|
|
|
// API: Get all categories
|
|
if (pathname === '/api/mcp-templates/categories' && req.method === 'GET') {
|
|
const categories = McpTemplatesDb.getAllCategories();
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, categories }));
|
|
return true;
|
|
}
|
|
|
|
// API: Get templates by category
|
|
if (pathname.startsWith('/api/mcp-templates/category/') && req.method === 'GET') {
|
|
const category = decodeURIComponent(pathname.split('/api/mcp-templates/category/')[1]);
|
|
const templates = McpTemplatesDb.getTemplatesByCategory(category);
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: true, templates }));
|
|
return true;
|
|
}
|
|
|
|
// API: Install template to project or global
|
|
if (pathname === '/api/mcp-templates/install' && req.method === 'POST') {
|
|
handlePostRequest(req, res, async (body) => {
|
|
const { templateName, projectPath, scope } = body;
|
|
if (!templateName) {
|
|
return { error: 'templateName is required', status: 400 };
|
|
}
|
|
|
|
const template = McpTemplatesDb.getTemplateByName(templateName);
|
|
if (!template) {
|
|
return { error: 'Template not found', status: 404 };
|
|
}
|
|
|
|
// Install to global or project
|
|
if (scope === 'global') {
|
|
return addGlobalMcpServer(templateName, template.serverConfig);
|
|
} else {
|
|
if (!projectPath) {
|
|
return { error: 'projectPath is required for project scope', status: 400 };
|
|
}
|
|
return addMcpServerToProject(projectPath, templateName, template.serverConfig);
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|