mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-09 19:09:45 +00:00
feat: Ability to add and control user level MCP Servers
This commit is contained in:
@@ -84,6 +84,84 @@ async function spawnClaude(command, options = {}, ws) {
|
|||||||
// Add basic flags
|
// Add basic flags
|
||||||
args.push('--output-format', 'stream-json', '--verbose');
|
args.push('--output-format', 'stream-json', '--verbose');
|
||||||
|
|
||||||
|
// Add MCP config flag only if MCP servers are configured
|
||||||
|
try {
|
||||||
|
console.log('🔍 Starting MCP config check...');
|
||||||
|
// Use already imported modules (fs.promises is imported as fs, path, os)
|
||||||
|
const fsSync = await import('fs'); // Import synchronous fs methods
|
||||||
|
console.log('✅ Successfully imported fs sync methods');
|
||||||
|
|
||||||
|
// Check for MCP config in ~/.claude.json
|
||||||
|
const claudeConfigPath = path.join(os.homedir(), '.claude.json');
|
||||||
|
|
||||||
|
console.log(`🔍 Checking for MCP configs in: ${claudeConfigPath}`);
|
||||||
|
console.log(` Claude config exists: ${fsSync.existsSync(claudeConfigPath)}`);
|
||||||
|
|
||||||
|
let hasMcpServers = false;
|
||||||
|
|
||||||
|
// Check Claude config for MCP servers
|
||||||
|
if (fsSync.existsSync(claudeConfigPath)) {
|
||||||
|
try {
|
||||||
|
const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8'));
|
||||||
|
|
||||||
|
// Check global MCP servers
|
||||||
|
if (claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) {
|
||||||
|
console.log(`✅ Found ${Object.keys(claudeConfig.mcpServers).length} global MCP servers`);
|
||||||
|
hasMcpServers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check project-specific MCP servers
|
||||||
|
if (!hasMcpServers && claudeConfig.claudeProjects) {
|
||||||
|
const currentProjectPath = process.cwd();
|
||||||
|
const projectConfig = claudeConfig.claudeProjects[currentProjectPath];
|
||||||
|
if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) {
|
||||||
|
console.log(`✅ Found ${Object.keys(projectConfig.mcpServers).length} project MCP servers`);
|
||||||
|
hasMcpServers = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`❌ Failed to parse Claude config:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 hasMcpServers result: ${hasMcpServers}`);
|
||||||
|
|
||||||
|
if (hasMcpServers) {
|
||||||
|
// Use Claude config file if it has MCP servers
|
||||||
|
let configPath = null;
|
||||||
|
|
||||||
|
if (fsSync.existsSync(claudeConfigPath)) {
|
||||||
|
try {
|
||||||
|
const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8'));
|
||||||
|
|
||||||
|
// Check if we have any MCP servers (global or project-specific)
|
||||||
|
const hasGlobalServers = claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0;
|
||||||
|
const currentProjectPath = process.cwd();
|
||||||
|
const projectConfig = claudeConfig.claudeProjects && claudeConfig.claudeProjects[currentProjectPath];
|
||||||
|
const hasProjectServers = projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0;
|
||||||
|
|
||||||
|
if (hasGlobalServers || hasProjectServers) {
|
||||||
|
configPath = claudeConfigPath;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// No valid config found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPath) {
|
||||||
|
console.log(`📡 Adding MCP config: ${configPath}`);
|
||||||
|
args.push('--mcp-config', configPath);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ MCP servers detected but no valid config file found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If there's any error checking for MCP configs, don't add the flag
|
||||||
|
console.log('❌ MCP config check failed:', error.message);
|
||||||
|
console.log('📍 Error stack:', error.stack);
|
||||||
|
console.log('Note: MCP config check failed, proceeding without MCP support');
|
||||||
|
}
|
||||||
|
|
||||||
// Add model for new sessions
|
// Add model for new sessions
|
||||||
if (!resume) {
|
if (!resume) {
|
||||||
args.push('--model', 'sonnet');
|
args.push('--model', 'sonnet');
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { getProjects, getSessions, getSessionMessages, renameProject, deleteSess
|
|||||||
import { spawnClaude, abortClaudeSession } from './claude-cli.js';
|
import { spawnClaude, abortClaudeSession } from './claude-cli.js';
|
||||||
import gitRoutes from './routes/git.js';
|
import gitRoutes from './routes/git.js';
|
||||||
import authRoutes from './routes/auth.js';
|
import authRoutes from './routes/auth.js';
|
||||||
|
import mcpRoutes from './routes/mcp.js';
|
||||||
import { initializeDatabase } from './database/db.js';
|
import { initializeDatabase } from './database/db.js';
|
||||||
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
||||||
|
|
||||||
@@ -131,17 +132,6 @@ async function setupProjectsWatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the first non-localhost IP address
|
// Get the first non-localhost IP address
|
||||||
function getServerIP() {
|
|
||||||
const interfaces = os.networkInterfaces();
|
|
||||||
for (const name of Object.keys(interfaces)) {
|
|
||||||
for (const iface of interfaces[name]) {
|
|
||||||
if (iface.family === 'IPv4' && !iface.internal) {
|
|
||||||
return iface.address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'localhost';
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
@@ -183,14 +173,16 @@ app.use('/api/auth', authRoutes);
|
|||||||
// Git API Routes (protected)
|
// Git API Routes (protected)
|
||||||
app.use('/api/git', authenticateToken, gitRoutes);
|
app.use('/api/git', authenticateToken, gitRoutes);
|
||||||
|
|
||||||
|
// MCP API Routes (protected)
|
||||||
|
app.use('/api/mcp', authenticateToken, mcpRoutes);
|
||||||
|
|
||||||
// Static files served after API routes
|
// Static files served after API routes
|
||||||
app.use(express.static(path.join(__dirname, '../dist')));
|
app.use(express.static(path.join(__dirname, '../dist')));
|
||||||
|
|
||||||
// API Routes (protected)
|
// API Routes (protected)
|
||||||
app.get('/api/config', authenticateToken, (req, res) => {
|
app.get('/api/config', authenticateToken, (req, res) => {
|
||||||
// Always use the server's actual IP and port for WebSocket connections
|
// Always use the server's actual IP and port for WebSocket connections
|
||||||
const serverIP = getServerIP();
|
const host = req.headers.host || `${req.hostname}:${PORT}`;
|
||||||
const host = `${serverIP}:${PORT}`;
|
|
||||||
const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws';
|
const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws';
|
||||||
|
|
||||||
console.log('Config API called - Returning host:', host, 'Protocol:', protocol);
|
console.log('Config API called - Returning host:', host, 'Protocol:', protocol);
|
||||||
@@ -428,8 +420,6 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|||||||
|
|
||||||
const files = await getFileTree(actualPath, 3, 0, true);
|
const files = await getFileTree(actualPath, 3, 0, true);
|
||||||
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
||||||
console.log('📄 Found', files.length, 'files/folders, including', hiddenFiles.length, 'hidden files');
|
|
||||||
console.log('🔍 Hidden files:', hiddenFiles.map(f => f.name));
|
|
||||||
res.json(files);
|
res.json(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ File tree error:', error.message);
|
console.error('❌ File tree error:', error.message);
|
||||||
@@ -1006,4 +996,4 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer();
|
startServer();
|
||||||
|
|||||||
286
server/routes/mcp.js
Normal file
286
server/routes/mcp.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Claude CLI command routes
|
||||||
|
|
||||||
|
// GET /api/mcp/cli/list - List MCP servers using Claude CLI
|
||||||
|
router.get('/cli/list', async (req, res) => {
|
||||||
|
try {
|
||||||
|
console.log('📋 Listing MCP servers using Claude CLI');
|
||||||
|
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const { promisify } = await import('util');
|
||||||
|
const exec = promisify(spawn);
|
||||||
|
|
||||||
|
const process = spawn('claude', ['mcp', 'list', '-s', 'user'], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
res.json({ success: true, output: stdout, servers: parseClaudeListOutput(stdout) });
|
||||||
|
} else {
|
||||||
|
console.error('Claude CLI error:', stderr);
|
||||||
|
res.status(500).json({ error: 'Claude CLI command failed', details: stderr });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
console.error('Error running Claude CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing MCP servers via CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to list MCP servers', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/mcp/cli/add - Add MCP server using Claude CLI
|
||||||
|
router.post('/cli/add', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body;
|
||||||
|
|
||||||
|
console.log('➕ Adding MCP server using Claude CLI:', name);
|
||||||
|
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
|
||||||
|
let cliArgs = ['mcp', 'add'];
|
||||||
|
|
||||||
|
if (type === 'http') {
|
||||||
|
cliArgs.push('--transport', 'http', name, '-s', 'user', url);
|
||||||
|
// Add headers if provided
|
||||||
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
|
cliArgs.push('--header', `${key}: ${value}`);
|
||||||
|
});
|
||||||
|
} else if (type === 'sse') {
|
||||||
|
cliArgs.push('--transport', 'sse', name, '-s', 'user', url);
|
||||||
|
// Add headers if provided
|
||||||
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
|
cliArgs.push('--header', `${key}: ${value}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// stdio (default): claude mcp add <name> -s user <command> [args...]
|
||||||
|
cliArgs.push(name, '-s', 'user');
|
||||||
|
// Add environment variables
|
||||||
|
Object.entries(env).forEach(([key, value]) => {
|
||||||
|
cliArgs.push('-e', `${key}=${value}`);
|
||||||
|
});
|
||||||
|
cliArgs.push(command);
|
||||||
|
if (args && args.length > 0) {
|
||||||
|
cliArgs.push(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 Running Claude CLI command:', 'claude', cliArgs.join(' '));
|
||||||
|
|
||||||
|
const process = spawn('claude', cliArgs, {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully` });
|
||||||
|
} else {
|
||||||
|
console.error('Claude CLI error:', stderr);
|
||||||
|
res.status(400).json({ error: 'Claude CLI command failed', details: stderr });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
console.error('Error running Claude CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding MCP server via CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to add MCP server', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/mcp/cli/remove/:name - Remove MCP server using Claude CLI
|
||||||
|
router.delete('/cli/remove/:name', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name } = req.params;
|
||||||
|
|
||||||
|
console.log('🗑️ Removing MCP server using Claude CLI:', name);
|
||||||
|
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
|
||||||
|
const process = spawn('claude', ['mcp', 'remove', '-s', 'user', name], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
res.json({ success: true, output: stdout, message: `MCP server "${name}" removed successfully` });
|
||||||
|
} else {
|
||||||
|
console.error('Claude CLI error:', stderr);
|
||||||
|
res.status(400).json({ error: 'Claude CLI command failed', details: stderr });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
console.error('Error running Claude CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing MCP server via CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to remove MCP server', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/mcp/cli/get/:name - Get MCP server details using Claude CLI
|
||||||
|
router.get('/cli/get/:name', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name } = req.params;
|
||||||
|
|
||||||
|
console.log('📄 Getting MCP server details using Claude CLI:', name);
|
||||||
|
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
|
||||||
|
const process = spawn('claude', ['mcp', 'get', '-s', 'user', name], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
res.json({ success: true, output: stdout, server: parseClaudeGetOutput(stdout) });
|
||||||
|
} else {
|
||||||
|
console.error('Claude CLI error:', stderr);
|
||||||
|
res.status(404).json({ error: 'Claude CLI command failed', details: stderr });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
console.error('Error running Claude CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting MCP server details via CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get MCP server details', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions to parse Claude CLI output
|
||||||
|
function parseClaudeListOutput(output) {
|
||||||
|
// Parse the output from 'claude mcp list' command
|
||||||
|
// Format: "name: command/url" or "name: url (TYPE)"
|
||||||
|
const servers = [];
|
||||||
|
const lines = output.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes(':')) {
|
||||||
|
const colonIndex = line.indexOf(':');
|
||||||
|
const name = line.substring(0, colonIndex).trim();
|
||||||
|
const rest = line.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
|
let type = 'stdio'; // default type
|
||||||
|
|
||||||
|
// Check if it has transport type in parentheses like "(SSE)" or "(HTTP)"
|
||||||
|
const typeMatch = rest.match(/\((\w+)\)\s*$/);
|
||||||
|
if (typeMatch) {
|
||||||
|
type = typeMatch[1].toLowerCase();
|
||||||
|
} else if (rest.startsWith('http://') || rest.startsWith('https://')) {
|
||||||
|
// If it's a URL but no explicit type, assume HTTP
|
||||||
|
type = 'http';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
servers.push({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 Parsed Claude CLI servers:', servers);
|
||||||
|
return servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseClaudeGetOutput(output) {
|
||||||
|
// Parse the output from 'claude mcp get <name>' command
|
||||||
|
// This is a simple parser - might need adjustment based on actual output format
|
||||||
|
try {
|
||||||
|
// Try to extract JSON if present
|
||||||
|
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
||||||
|
if (jsonMatch) {
|
||||||
|
return JSON.parse(jsonMatch[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, parse as text
|
||||||
|
const server = { raw_output: output };
|
||||||
|
const lines = output.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('Name:')) {
|
||||||
|
server.name = line.split(':')[1]?.trim();
|
||||||
|
} else if (line.includes('Type:')) {
|
||||||
|
server.type = line.split(':')[1]?.trim();
|
||||||
|
} else if (line.includes('Command:')) {
|
||||||
|
server.command = line.split(':')[1]?.trim();
|
||||||
|
} else if (line.includes('URL:')) {
|
||||||
|
server.url = line.split(':')[1]?.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return server;
|
||||||
|
} catch (error) {
|
||||||
|
return { raw_output: output, parse_error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default router;
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user