diff --git a/server/index.js b/server/index.js index f61ccb90..10f70f05 100755 --- a/server/index.js +++ b/server/index.js @@ -44,10 +44,9 @@ import cors from 'cors'; import { promises as fsPromises } from 'fs'; import { spawn } from 'child_process'; import pty from 'node-pty'; -import fetch from 'node-fetch'; import mime from 'mime-types'; -import { getProjects, getSessions, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js'; +import { getProjects, getSessions, renameProject, deleteSession, deleteProject, extractProjectDirectory, clearProjectDirectoryCache, searchConversations } from './projects.js'; import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js'; import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js'; import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js'; @@ -55,7 +54,6 @@ import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGemini import sessionManager from './sessionManager.js'; import gitRoutes from './routes/git.js'; import authRoutes from './routes/auth.js'; -import mcpRoutes from './routes/mcp.js'; import cursorRoutes from './routes/cursor.js'; import taskmasterRoutes from './routes/taskmaster.js'; import mcpUtilsRoutes from './routes/mcp-utils.js'; @@ -367,9 +365,6 @@ app.use('/api/projects', authenticateToken, projectsRoutes); // Git API Routes (protected) app.use('/api/git', authenticateToken, gitRoutes); -// MCP API Routes (protected) -app.use('/api/mcp', authenticateToken, mcpRoutes); - // Cursor API Routes (protected) app.use('/api/cursor', authenticateToken, cursorRoutes); @@ -587,23 +582,6 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => } }); -// Create project endpoint -app.post('/api/projects/create', authenticateToken, async (req, res) => { - try { - const { path: projectPath } = req.body; - - if (!projectPath || !projectPath.trim()) { - return res.status(400).json({ error: 'Project path is required' }); - } - - const project = await addProjectManually(projectPath.trim()); - res.json({ success: true, project }); - } catch (error) { - console.error('Error creating project:', error); - res.status(500).json({ error: error.message }); - } -}); - // Search conversations content (SSE streaming) app.get('/api/search/conversations', authenticateToken, async (req, res) => { const query = typeof req.query.q === 'string' ? req.query.q.trim() : ''; diff --git a/server/modules/providers/provider.routes.ts b/server/modules/providers/provider.routes.ts index 7d2ac038..5419f3d4 100644 --- a/server/modules/providers/provider.routes.ts +++ b/server/modules/providers/provider.routes.ts @@ -180,19 +180,6 @@ router.post( }), ); -router.put( - '/:provider/mcp/servers/:name', - asyncHandler(async (req: Request, res: Response) => { - const provider = parseProvider(req.params.provider); - const payload = parseMcpUpsertPayload({ - ...((req.body && typeof req.body === 'object') ? req.body as Record : {}), - name: readPathParam(req.params.name, 'name'), - }); - const server = await providerMcpService.upsertProviderMcpServer(provider, payload); - res.json(createApiSuccessResponse({ server })); - }), -); - router.delete( '/:provider/mcp/servers/:name', asyncHandler(async (req: Request, res: Response) => { @@ -208,22 +195,6 @@ router.delete( }), ); -router.post( - '/:provider/mcp/servers/:name/run', - asyncHandler(async (req: Request, res: Response) => { - const provider = parseProvider(req.params.provider); - const body = (req.body as Record | undefined) ?? {}; - const scope = parseMcpScope(body.scope ?? req.query.scope); - const workspacePath = readOptionalQueryString(body.workspacePath ?? req.query.workspacePath); - const result = await providerMcpService.runProviderMcpServer(provider, { - name: readPathParam(req.params.name, 'name'), - scope, - workspacePath, - }); - res.json(createApiSuccessResponse(result)); - }), -); - router.post( '/mcp/servers/global', asyncHandler(async (req: Request, res: Response) => { diff --git a/server/modules/providers/services/mcp.service.ts b/server/modules/providers/services/mcp.service.ts index 54592b60..bffb52de 100644 --- a/server/modules/providers/services/mcp.service.ts +++ b/server/modules/providers/services/mcp.service.ts @@ -60,25 +60,6 @@ export const providerMcpService = { return provider.mcp.removeServer(input); }, - /** - * Runs one provider MCP server probe. - */ - async runProviderMcpServer( - providerName: string, - input: { name: string; scope?: McpScope; workspacePath?: string }, - ): Promise<{ - provider: LLMProvider; - name: string; - scope: McpScope; - transport: 'stdio' | 'http' | 'sse'; - reachable: boolean; - statusCode?: number; - error?: string; - }> { - const provider = providerRegistry.resolveProvider(providerName); - return provider.mcp.runServer(input); - }, - /** * Adds one HTTP/stdio MCP server to every provider. */ diff --git a/server/modules/providers/shared/mcp/mcp.provider.ts b/server/modules/providers/shared/mcp/mcp.provider.ts index 46c00a56..96cc7f25 100644 --- a/server/modules/providers/shared/mcp/mcp.provider.ts +++ b/server/modules/providers/shared/mcp/mcp.provider.ts @@ -1,8 +1,5 @@ -import { once } from 'node:events'; import path from 'node:path'; -import spawn from 'cross-spawn'; - import type { IProviderMcp } from '@/shared/interfaces.js'; import type { LLMProvider, McpScope, McpTransport, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js'; import { AppError } from '@/shared/utils.js'; @@ -22,74 +19,6 @@ const normalizeServerName = (name: string): string => { return normalized; }; -const runStdioServerProbe = async ( - server: ProviderMcpServer, - workspacePath: string, -): Promise<{ reachable: boolean; error?: string }> => { - if (!server.command) { - return { reachable: false, error: 'Missing stdio command.' }; - } - - try { - const child = spawn(server.command, server.args ?? [], { - cwd: server.cwd ? path.resolve(workspacePath, server.cwd) : workspacePath, - env: { - ...process.env, - ...(server.env ?? {}), - }, - stdio: ['ignore', 'pipe', 'pipe'], - }); - - const timeout = setTimeout(() => { - if (!child.killed && child.exitCode === null) { - child.kill('SIGTERM'); - } - }, 1_500); - - const errorPromise = once(child, 'error').then(([error]) => { - throw error; - }); - const closePromise = once(child, 'close'); - await Promise.race([closePromise, errorPromise]); - clearTimeout(timeout); - - if (typeof child.exitCode === 'number' && child.exitCode !== 0) { - return { - reachable: false, - error: `Process exited with code ${child.exitCode}.`, - }; - } - - return { reachable: true }; - } catch (error) { - return { - reachable: false, - error: error instanceof Error ? error.message : 'Failed to start stdio process', - }; - } -}; - -const runHttpServerProbe = async ( - url: string, -): Promise<{ reachable: boolean; statusCode?: number; error?: string }> => { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3_000); - try { - const response = await fetch(url, { method: 'GET', signal: controller.signal }); - clearTimeout(timeout); - return { - reachable: true, - statusCode: response.status, - }; - } catch (error) { - clearTimeout(timeout); - return { - reachable: false, - error: error instanceof Error ? error.message : 'Network probe failed', - }; - } -}; - /** * Shared MCP provider for provider-specific config readers/writers. */ @@ -182,63 +111,6 @@ export abstract class McpProvider implements IProviderMcp { return { removed, provider: this.provider, name: normalizedName, scope }; } - async runServer( - input: { name: string; scope?: McpScope; workspacePath?: string }, - ): Promise<{ - provider: LLMProvider; - name: string; - scope: McpScope; - transport: McpTransport; - reachable: boolean; - statusCode?: number; - error?: string; - }> { - const scope = input.scope ?? 'project'; - this.assertScope(scope); - - const workspacePath = resolveWorkspacePath(input.workspacePath); - const normalizedName = normalizeServerName(input.name); - const scopedServers = await this.readScopedServers(scope, workspacePath); - const rawConfig = scopedServers[normalizedName]; - if (!rawConfig || typeof rawConfig !== 'object') { - throw new AppError(`MCP server "${normalizedName}" was not found.`, { - code: 'MCP_SERVER_NOT_FOUND', - statusCode: 404, - }); - } - - const normalized = this.normalizeServerConfig(scope, normalizedName, rawConfig); - if (!normalized) { - throw new AppError(`MCP server "${normalizedName}" has an invalid configuration.`, { - code: 'MCP_SERVER_INVALID_CONFIG', - statusCode: 400, - }); - } - - if (normalized.transport === 'stdio') { - const result = await runStdioServerProbe(normalized, workspacePath); - return { - provider: this.provider, - name: normalizedName, - scope, - transport: normalized.transport, - reachable: result.reachable, - error: result.error, - }; - } - - const result = await runHttpServerProbe(normalized.url ?? ''); - return { - provider: this.provider, - name: normalizedName, - scope, - transport: normalized.transport, - reachable: result.reachable, - statusCode: result.statusCode, - error: result.error, - }; - } - protected abstract readScopedServers( scope: McpScope, workspacePath: string, diff --git a/server/modules/providers/tests/mcp.test.ts b/server/modules/providers/tests/mcp.test.ts index 495f1027..a64914d6 100644 --- a/server/modules/providers/tests/mcp.test.ts +++ b/server/modules/providers/tests/mcp.test.ts @@ -1,6 +1,5 @@ import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; -import http from 'node:http'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; @@ -292,62 +291,3 @@ test('providerMcpService global adder writes to all providers and rejects unsupp } }); -/** - * This test covers "run" behavior for both stdio and http MCP servers. - */ -test('providerMcpService runProviderServer probes stdio and http MCP servers', { concurrency: false }, async () => { - const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'llm-mcp-run-')); - const workspacePath = path.join(tempRoot, 'workspace'); - await fs.mkdir(workspacePath, { recursive: true }); - - const restoreHomeDir = patchHomeDir(tempRoot); - const server = http.createServer((_req, res) => { - res.statusCode = 200; - res.end('ok'); - }); - - try { - await new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve())); - const address = server.address(); - assert.ok(address && typeof address === 'object'); - const url = `http://127.0.0.1:${address.port}/mcp`; - - await providerMcpService.upsertProviderMcpServer('gemini', { - name: 'probe-http', - scope: 'project', - transport: 'http', - url, - workspacePath, - }); - - await providerMcpService.upsertProviderMcpServer('cursor', { - name: 'probe-stdio', - scope: 'project', - transport: 'stdio', - command: process.execPath, - args: ['-e', 'process.exit(0)'], - workspacePath, - }); - - const httpProbe = await providerMcpService.runProviderMcpServer('gemini', { - name: 'probe-http', - scope: 'project', - workspacePath, - }); - assert.equal(httpProbe.reachable, true); - assert.equal(httpProbe.transport, 'http'); - - const stdioProbe = await providerMcpService.runProviderMcpServer('cursor', { - name: 'probe-stdio', - scope: 'project', - workspacePath, - }); - assert.equal(stdioProbe.reachable, true); - assert.equal(stdioProbe.transport, 'stdio'); - } finally { - server.close(); - restoreHomeDir(); - await fs.rm(tempRoot, { recursive: true, force: true }); - } -}); - diff --git a/server/routes/codex.js b/server/routes/codex.js index 3855548e..06630414 100644 --- a/server/routes/codex.js +++ b/server/routes/codex.js @@ -1,73 +1,9 @@ import express from 'express'; -import { spawn } from 'child_process'; -import { promises as fs } from 'fs'; -import path from 'path'; -import os from 'os'; -import TOML from '@iarna/toml'; -import { getCodexSessions, deleteCodexSession } from '../projects.js'; -import { applyCustomSessionNames, sessionNamesDb } from '../database/db.js'; +import { deleteCodexSession } from '../projects.js'; +import { sessionNamesDb } from '../database/db.js'; const router = express.Router(); -function createCliResponder(res) { - let responded = false; - return (status, payload) => { - if (responded || res.headersSent) { - return; - } - responded = true; - res.status(status).json(payload); - }; -} - -router.get('/config', async (req, res) => { - try { - const configPath = path.join(os.homedir(), '.codex', 'config.toml'); - const content = await fs.readFile(configPath, 'utf8'); - const config = TOML.parse(content); - - res.json({ - success: true, - config: { - model: config.model || null, - mcpServers: config.mcp_servers || {}, - approvalMode: config.approval_mode || 'suggest' - } - }); - } catch (error) { - if (error.code === 'ENOENT') { - res.json({ - success: true, - config: { - model: null, - mcpServers: {}, - approvalMode: 'suggest' - } - }); - } else { - console.error('Error reading Codex config:', error); - res.status(500).json({ success: false, error: error.message }); - } - } -}); - -router.get('/sessions', async (req, res) => { - try { - const { projectPath } = req.query; - - if (!projectPath) { - return res.status(400).json({ success: false, error: 'projectPath query parameter required' }); - } - - const sessions = await getCodexSessions(projectPath); - applyCustomSessionNames(sessions, 'codex'); - res.json({ success: true, sessions }); - } catch (error) { - console.error('Error fetching Codex sessions:', error); - res.status(500).json({ success: false, error: error.message }); - } -}); - router.delete('/sessions/:sessionId', async (req, res) => { try { const { sessionId } = req.params; @@ -80,250 +16,4 @@ router.delete('/sessions/:sessionId', async (req, res) => { } }); -// MCP Server Management Routes - -router.get('/mcp/cli/list', async (req, res) => { - try { - const respond = createCliResponder(res); - const proc = spawn('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] }); - - let stdout = ''; - let stderr = ''; - - proc.stdout?.on('data', (data) => { stdout += data.toString(); }); - proc.stderr?.on('data', (data) => { stderr += data.toString(); }); - - proc.on('close', (code) => { - if (code === 0) { - respond(200, { success: true, output: stdout, servers: parseCodexListOutput(stdout) }); - } else { - respond(500, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); - } - }); - - proc.on('error', (error) => { - const isMissing = error?.code === 'ENOENT'; - respond(isMissing ? 503 : 500, { - error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', - details: error.message, - code: error.code - }); - }); - } catch (error) { - res.status(500).json({ error: 'Failed to list MCP servers', details: error.message }); - } -}); - -router.post('/mcp/cli/add', async (req, res) => { - try { - const { name, command, args = [], env = {} } = req.body; - - if (!name || !command) { - return res.status(400).json({ error: 'name and command are required' }); - } - - // Build: codex mcp add [-e KEY=VAL]... -- [args...] - let cliArgs = ['mcp', 'add', name]; - - Object.entries(env).forEach(([key, value]) => { - cliArgs.push('-e', `${key}=${value}`); - }); - - cliArgs.push('--', command); - - if (args && args.length > 0) { - cliArgs.push(...args); - } - - const respond = createCliResponder(res); - const proc = spawn('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] }); - - let stdout = ''; - let stderr = ''; - - proc.stdout?.on('data', (data) => { stdout += data.toString(); }); - proc.stderr?.on('data', (data) => { stderr += data.toString(); }); - - proc.on('close', (code) => { - if (code === 0) { - respond(200, { success: true, output: stdout, message: `MCP server "${name}" added successfully` }); - } else { - respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); - } - }); - - proc.on('error', (error) => { - const isMissing = error?.code === 'ENOENT'; - respond(isMissing ? 503 : 500, { - error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', - details: error.message, - code: error.code - }); - }); - } catch (error) { - res.status(500).json({ error: 'Failed to add MCP server', details: error.message }); - } -}); - -router.delete('/mcp/cli/remove/:name', async (req, res) => { - try { - const { name } = req.params; - - const respond = createCliResponder(res); - const proc = spawn('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] }); - - let stdout = ''; - let stderr = ''; - - proc.stdout?.on('data', (data) => { stdout += data.toString(); }); - proc.stderr?.on('data', (data) => { stderr += data.toString(); }); - - proc.on('close', (code) => { - if (code === 0) { - respond(200, { success: true, output: stdout, message: `MCP server "${name}" removed successfully` }); - } else { - respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); - } - }); - - proc.on('error', (error) => { - const isMissing = error?.code === 'ENOENT'; - respond(isMissing ? 503 : 500, { - error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', - details: error.message, - code: error.code - }); - }); - } catch (error) { - res.status(500).json({ error: 'Failed to remove MCP server', details: error.message }); - } -}); - -router.get('/mcp/cli/get/:name', async (req, res) => { - try { - const { name } = req.params; - - const respond = createCliResponder(res); - const proc = spawn('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] }); - - let stdout = ''; - let stderr = ''; - - proc.stdout?.on('data', (data) => { stdout += data.toString(); }); - proc.stderr?.on('data', (data) => { stderr += data.toString(); }); - - proc.on('close', (code) => { - if (code === 0) { - respond(200, { success: true, output: stdout, server: parseCodexGetOutput(stdout) }); - } else { - respond(404, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` }); - } - }); - - proc.on('error', (error) => { - const isMissing = error?.code === 'ENOENT'; - respond(isMissing ? 503 : 500, { - error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI', - details: error.message, - code: error.code - }); - }); - } catch (error) { - res.status(500).json({ error: 'Failed to get MCP server details', details: error.message }); - } -}); - -router.get('/mcp/config/read', async (req, res) => { - try { - const configPath = path.join(os.homedir(), '.codex', 'config.toml'); - - let configData = null; - - try { - const fileContent = await fs.readFile(configPath, 'utf8'); - configData = TOML.parse(fileContent); - } catch (error) { - // Config file doesn't exist - } - - if (!configData) { - return res.json({ success: true, configPath, servers: [] }); } - - const servers = []; - - if (configData.mcp_servers && typeof configData.mcp_servers === 'object') { - for (const [name, config] of Object.entries(configData.mcp_servers)) { - servers.push({ - id: name, - name: name, - type: 'stdio', - scope: 'user', - config: { - command: config.command || '', - args: config.args || [], - env: config.env || {} - }, - raw: config - }); - } - } - - res.json({ success: true, configPath, servers }); - } catch (error) { - res.status(500).json({ error: 'Failed to read Codex configuration', details: error.message }); - } -}); - -function parseCodexListOutput(output) { - 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(); - - if (!name) continue; - - const rest = line.substring(colonIndex + 1).trim(); - let description = rest; - let status = 'unknown'; - - if (rest.includes('✓') || rest.includes('✗')) { - const statusMatch = rest.match(/(.*?)\s*-\s*([✓✗].*)$/); - if (statusMatch) { - description = statusMatch[1].trim(); - status = statusMatch[2].includes('✓') ? 'connected' : 'failed'; - } - } - - servers.push({ name, type: 'stdio', status, description }); - } - } - - return servers; -} - -function parseCodexGetOutput(output) { - try { - const jsonMatch = output.match(/\{[\s\S]*\}/); - if (jsonMatch) { - return JSON.parse(jsonMatch[0]); - } - - 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(); - } - - return server; - } catch (error) { - return { raw_output: output, parse_error: error.message }; - } -} - export default router; diff --git a/server/routes/commands.js b/server/routes/commands.js index 4ce3c4c0..4b791564 100644 --- a/server/routes/commands.js +++ b/server/routes/commands.js @@ -451,55 +451,6 @@ router.post('/list', async (req, res) => { } }); -/** - * POST /api/commands/load - * Load a specific command file and return its content and metadata - */ -router.post('/load', async (req, res) => { - try { - const { commandPath } = req.body; - - if (!commandPath) { - return res.status(400).json({ - error: 'Command path is required' - }); - } - - // Security: Prevent path traversal - const resolvedPath = path.resolve(commandPath); - if (!resolvedPath.startsWith(path.resolve(os.homedir())) && - !resolvedPath.includes('.claude/commands')) { - return res.status(403).json({ - error: 'Access denied', - message: 'Command must be in .claude/commands directory' - }); - } - - // Read and parse the command file - const content = await fs.readFile(commandPath, 'utf8'); - const { data: metadata, content: commandContent } = parseFrontmatter(content); - - res.json({ - path: commandPath, - metadata, - content: commandContent - }); - } catch (error) { - if (error.code === 'ENOENT') { - return res.status(404).json({ - error: 'Command not found', - message: `Command file not found: ${req.body.commandPath}` - }); - } - - console.error('Error loading command:', error); - res.status(500).json({ - error: 'Failed to load command', - message: error.message - }); - } -}); - /** * POST /api/commands/execute * Execute a command with argument replacement diff --git a/server/routes/cursor.js b/server/routes/cursor.js index 9842e7a6..01af7b5a 100644 --- a/server/routes/cursor.js +++ b/server/routes/cursor.js @@ -2,11 +2,7 @@ import express from 'express'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; -import sqlite3 from 'sqlite3'; -import { open } from 'sqlite'; -import crypto from 'crypto'; import { CURSOR_MODELS } from '../../shared/modelConstants.js'; -import { applyCustomSessionNames } from '../database/db.js'; const router = express.Router(); @@ -14,20 +10,20 @@ const router = express.Router(); router.get('/config', async (req, res) => { try { const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json'); - + try { const configContent = await fs.readFile(configPath, 'utf8'); const config = JSON.parse(configContent); - + res.json({ success: true, - config: config, - path: configPath + config, + path: configPath, }); } catch (error) { // Config doesn't exist or is invalid console.log('Cursor config not found or invalid:', error.message); - + // Return default config res.json({ success: true, @@ -35,381 +31,23 @@ router.get('/config', async (req, res) => { version: 1, model: { modelId: CURSOR_MODELS.DEFAULT, - displayName: "GPT-5" + displayName: 'GPT-5', }, permissions: { allow: [], - deny: [] - } + deny: [], + }, }, - isDefault: true + isDefault: true, }); } } catch (error) { console.error('Error reading Cursor config:', error); - res.status(500).json({ - error: 'Failed to read Cursor configuration', - details: error.message + res.status(500).json({ + error: 'Failed to read Cursor configuration', + details: error.message, }); } }); -// POST /api/cursor/config - Update Cursor CLI configuration -router.post('/config', async (req, res) => { - try { - const { permissions, model } = req.body; - const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json'); - - // Read existing config or create default - let config = { - version: 1, - editor: { - vimMode: false - }, - hasChangedDefaultModel: false, - privacyCache: { - ghostMode: false, - privacyMode: 3, - updatedAt: Date.now() - } - }; - - try { - const existing = await fs.readFile(configPath, 'utf8'); - config = JSON.parse(existing); - } catch (error) { - // Config doesn't exist, use defaults - console.log('Creating new Cursor config'); - } - - // Update permissions if provided - if (permissions) { - config.permissions = { - allow: permissions.allow || [], - deny: permissions.deny || [] - }; - } - - // Update model if provided - if (model) { - config.model = model; - config.hasChangedDefaultModel = true; - } - - // Ensure directory exists - const configDir = path.dirname(configPath); - await fs.mkdir(configDir, { recursive: true }); - - // Write updated config - await fs.writeFile(configPath, JSON.stringify(config, null, 2)); - - res.json({ - success: true, - config: config, - message: 'Cursor configuration updated successfully' - }); - } catch (error) { - console.error('Error updating Cursor config:', error); - res.status(500).json({ - error: 'Failed to update Cursor configuration', - details: error.message - }); - } -}); - -// GET /api/cursor/mcp - Read Cursor MCP servers configuration -router.get('/mcp', async (req, res) => { - try { - const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); - - try { - const mcpContent = await fs.readFile(mcpPath, 'utf8'); - const mcpConfig = JSON.parse(mcpContent); - - // Convert to UI-friendly format - const servers = []; - if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') { - for (const [name, config] of Object.entries(mcpConfig.mcpServers)) { - const server = { - id: name, - name: name, - type: 'stdio', - scope: 'cursor', - config: {}, - raw: config - }; - - // Determine transport type and extract config - if (config.command) { - server.type = 'stdio'; - server.config.command = config.command; - server.config.args = config.args || []; - server.config.env = config.env || {}; - } else if (config.url) { - server.type = config.transport || 'http'; - server.config.url = config.url; - server.config.headers = config.headers || {}; - } - - servers.push(server); - } - } - - res.json({ - success: true, - servers: servers, - path: mcpPath - }); - } catch (error) { - // MCP config doesn't exist - console.log('Cursor MCP config not found:', error.message); - res.json({ - success: true, - servers: [], - isDefault: true - }); - } - } catch (error) { - console.error('Error reading Cursor MCP config:', error); - res.status(500).json({ - error: 'Failed to read Cursor MCP configuration', - details: error.message - }); - } -}); - -// GET /api/cursor/sessions - Get Cursor sessions from SQLite database -router.get('/sessions', async (req, res) => { - try { - const { projectPath } = req.query; - - // Calculate cwdID hash for the project path (Cursor uses MD5 hash) - const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); - const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId); - - - // Check if the directory exists - try { - await fs.access(cursorChatsPath); - } catch (error) { - // No sessions for this project - return res.json({ - success: true, - sessions: [], - cwdId: cwdId, - path: cursorChatsPath - }); - } - - // List all session directories - const sessionDirs = await fs.readdir(cursorChatsPath); - const sessions = []; - - for (const sessionId of sessionDirs) { - const sessionPath = path.join(cursorChatsPath, sessionId); - const storeDbPath = path.join(sessionPath, 'store.db'); - let dbStatMtimeMs = null; - - try { - // Check if store.db exists - await fs.access(storeDbPath); - - // Capture store.db mtime as a reliable fallback timestamp (last activity) - try { - const stat = await fs.stat(storeDbPath); - dbStatMtimeMs = stat.mtimeMs; - } catch (_) {} - - // Open SQLite database - const db = await open({ - filename: storeDbPath, - driver: sqlite3.Database, - mode: sqlite3.OPEN_READONLY - }); - - // Get metadata from meta table - const metaRows = await db.all(` - SELECT key, value FROM meta - `); - - let sessionData = { - id: sessionId, - name: 'Untitled Session', - createdAt: null, - mode: null, - projectPath: projectPath, - lastMessage: null, - messageCount: 0 - }; - - // Parse meta table entries - for (const row of metaRows) { - if (row.value) { - try { - // Try to decode as hex-encoded JSON - const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/); - if (hexMatch) { - const jsonStr = Buffer.from(row.value, 'hex').toString('utf8'); - const data = JSON.parse(jsonStr); - - if (row.key === 'agent') { - sessionData.name = data.name || sessionData.name; - // Normalize createdAt to ISO string in milliseconds - let createdAt = data.createdAt; - if (typeof createdAt === 'number') { - if (createdAt < 1e12) { - createdAt = createdAt * 1000; // seconds -> ms - } - sessionData.createdAt = new Date(createdAt).toISOString(); - } else if (typeof createdAt === 'string') { - const n = Number(createdAt); - if (!Number.isNaN(n)) { - const ms = n < 1e12 ? n * 1000 : n; - sessionData.createdAt = new Date(ms).toISOString(); - } else { - // Assume it's already an ISO/date string - const d = new Date(createdAt); - sessionData.createdAt = isNaN(d.getTime()) ? null : d.toISOString(); - } - } else { - sessionData.createdAt = sessionData.createdAt || null; - } - sessionData.mode = data.mode; - sessionData.agentId = data.agentId; - sessionData.latestRootBlobId = data.latestRootBlobId; - } - } else { - // If not hex, use raw value for simple keys - if (row.key === 'name') { - sessionData.name = row.value.toString(); - } - } - } catch (e) { - console.log(`Could not parse meta value for key ${row.key}:`, e.message); - } - } - } - - // Get message count from JSON blobs only (actual messages, not DAG structure) - try { - const blobCount = await db.get(` - SELECT COUNT(*) as count - FROM blobs - WHERE substr(data, 1, 1) = X'7B' - `); - sessionData.messageCount = blobCount.count; - - // Get the most recent JSON blob for preview (actual message, not DAG structure) - const lastBlob = await db.get(` - SELECT data FROM blobs - WHERE substr(data, 1, 1) = X'7B' - ORDER BY rowid DESC - LIMIT 1 - `); - - if (lastBlob && lastBlob.data) { - try { - // Try to extract readable preview from blob (may contain binary with embedded JSON) - const raw = lastBlob.data.toString('utf8'); - let preview = ''; - // Attempt direct JSON parse - try { - const parsed = JSON.parse(raw); - if (parsed?.content) { - if (Array.isArray(parsed.content)) { - const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || ''; - preview = firstText; - } else if (typeof parsed.content === 'string') { - preview = parsed.content; - } - } - } catch (_) {} - if (!preview) { - // Strip non-printable and try to find JSON chunk - const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ''); - const s = cleaned; - const start = s.indexOf('{'); - const end = s.lastIndexOf('}'); - if (start !== -1 && end > start) { - const jsonStr = s.slice(start, end + 1); - try { - const parsed = JSON.parse(jsonStr); - if (parsed?.content) { - if (Array.isArray(parsed.content)) { - const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || ''; - preview = firstText; - } else if (typeof parsed.content === 'string') { - preview = parsed.content; - } - } - } catch (_) { - preview = s; - } - } else { - preview = s; - } - } - if (preview && preview.length > 0) { - sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : ''); - } - } catch (e) { - console.log('Could not parse blob data:', e.message); - } - } - } catch (e) { - console.log('Could not read blobs:', e.message); - } - - await db.close(); - - // Finalize createdAt: use parsed meta value when valid, else fall back to store.db mtime - if (!sessionData.createdAt) { - if (dbStatMtimeMs && Number.isFinite(dbStatMtimeMs)) { - sessionData.createdAt = new Date(dbStatMtimeMs).toISOString(); - } - } - - sessions.push(sessionData); - - } catch (error) { - console.log(`Could not read session ${sessionId}:`, error.message); - } - } - - // Fallback: ensure createdAt is a valid ISO string (use session directory mtime as last resort) - for (const s of sessions) { - if (!s.createdAt) { - try { - const sessionDir = path.join(cursorChatsPath, s.id); - const st = await fs.stat(sessionDir); - s.createdAt = new Date(st.mtimeMs).toISOString(); - } catch { - s.createdAt = new Date().toISOString(); - } - } - } - // Sort sessions by creation date (newest first) - sessions.sort((a, b) => { - if (!a.createdAt) return 1; - if (!b.createdAt) return -1; - return new Date(b.createdAt) - new Date(a.createdAt); - }); - - applyCustomSessionNames(sessions, 'cursor'); - - res.json({ - success: true, - sessions: sessions, - cwdId: cwdId, - path: cursorChatsPath - }); - - } catch (error) { - console.error('Error reading Cursor sessions:', error); - res.status(500).json({ - error: 'Failed to read Cursor sessions', - details: error.message - }); - } -}); export default router; diff --git a/server/routes/mcp-utils.js b/server/routes/mcp-utils.js index 8b3cd292..52312d2e 100644 --- a/server/routes/mcp-utils.js +++ b/server/routes/mcp-utils.js @@ -7,7 +7,7 @@ */ import express from 'express'; -import { detectTaskMasterMCPServer, getAllMCPServers } from '../utils/mcp-detector.js'; +import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js'; const router = express.Router(); @@ -28,21 +28,4 @@ router.get('/taskmaster-server', async (req, res) => { } }); -/** - * GET /api/mcp-utils/all-servers - * Get all configured MCP servers - */ -router.get('/all-servers', async (req, res) => { - try { - const result = await getAllMCPServers(); - res.json(result); - } catch (error) { - console.error('MCP servers detection error:', error); - res.status(500).json({ - error: 'Failed to get MCP servers', - message: error.message - }); - } -}); - -export default router; \ No newline at end of file +export default router; diff --git a/server/routes/mcp.js b/server/routes/mcp.js deleted file mode 100644 index 080be6ab..00000000 --- a/server/routes/mcp.js +++ /dev/null @@ -1,552 +0,0 @@ -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'], { - 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 = {}, scope = 'user', projectPath } = req.body; - - console.log(`➕ Adding MCP server using Claude CLI (${scope} scope):`, name); - - const { spawn } = await import('child_process'); - - let cliArgs = ['mcp', 'add']; - - // Add scope flag - cliArgs.push('--scope', scope); - - if (type === 'http') { - cliArgs.push('--transport', 'http', name, 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, url); - // Add headers if provided - Object.entries(headers).forEach(([key, value]) => { - cliArgs.push('--header', `${key}: ${value}`); - }); - } else { - // stdio (default): claude mcp add --scope user [args...] - cliArgs.push(name); - // 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(' ')); - - // For local scope, we need to run the command in the project directory - const spawnOptions = { - stdio: ['pipe', 'pipe', 'pipe'] - }; - - if (scope === 'local' && projectPath) { - spawnOptions.cwd = projectPath; - console.log('📁 Running in project directory:', projectPath); - } - - const process = spawn('claude', cliArgs, spawnOptions); - - 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 }); - } -}); - -// POST /api/mcp/cli/add-json - Add MCP server using JSON format -router.post('/cli/add-json', async (req, res) => { - try { - const { name, jsonConfig, scope = 'user', projectPath } = req.body; - - console.log('➕ Adding MCP server using JSON format:', name); - - // Validate and parse JSON config - let parsedConfig; - try { - parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig; - } catch (parseError) { - return res.status(400).json({ - error: 'Invalid JSON configuration', - details: parseError.message - }); - } - - // Validate required fields - if (!parsedConfig.type) { - return res.status(400).json({ - error: 'Invalid configuration', - details: 'Missing required field: type' - }); - } - - if (parsedConfig.type === 'stdio' && !parsedConfig.command) { - return res.status(400).json({ - error: 'Invalid configuration', - details: 'stdio type requires a command field' - }); - } - - if ((parsedConfig.type === 'http' || parsedConfig.type === 'sse') && !parsedConfig.url) { - return res.status(400).json({ - error: 'Invalid configuration', - details: `${parsedConfig.type} type requires a url field` - }); - } - - const { spawn } = await import('child_process'); - - // Build the command: claude mcp add-json --scope '' - const cliArgs = ['mcp', 'add-json', '--scope', scope, name]; - - // Add the JSON config as a properly formatted string - const jsonString = JSON.stringify(parsedConfig); - cliArgs.push(jsonString); - - console.log('🔧 Running Claude CLI command:', 'claude', cliArgs[0], cliArgs[1], cliArgs[2], cliArgs[3], cliArgs[4], jsonString); - - // For local scope, we need to run the command in the project directory - const spawnOptions = { - stdio: ['pipe', 'pipe', 'pipe'] - }; - - if (scope === 'local' && projectPath) { - spawnOptions.cwd = projectPath; - console.log('📁 Running in project directory:', projectPath); - } - - const process = spawn('claude', cliArgs, spawnOptions); - - 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 via JSON` }); - } 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 JSON:', 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; - const { scope } = req.query; // Get scope from query params - - // Handle the ID format (remove scope prefix if present) - let actualName = name; - let actualScope = scope; - - // If the name includes a scope prefix like "local:test", extract it - if (name.includes(':')) { - const [prefix, serverName] = name.split(':'); - actualName = serverName; - actualScope = actualScope || prefix; // Use prefix as scope if not provided in query - } - - console.log('🗑️ Removing MCP server using Claude CLI:', actualName, 'scope:', actualScope); - - const { spawn } = await import('child_process'); - - // Build command args based on scope - let cliArgs = ['mcp', 'remove']; - - // Add scope flag if it's local scope - if (actualScope === 'local') { - cliArgs.push('--scope', 'local'); - } else if (actualScope === 'user' || !actualScope) { - // User scope is default, but we can be explicit - cliArgs.push('--scope', 'user'); - } - - cliArgs.push(actualName); - - 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}" 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', 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 }); - } -}); - -// GET /api/mcp/config/read - Read MCP servers directly from Claude config files -router.get('/config/read', async (req, res) => { - try { - console.log('📖 Reading MCP servers from Claude config files'); - - const homeDir = os.homedir(); - const configPaths = [ - path.join(homeDir, '.claude.json'), - path.join(homeDir, '.claude', 'settings.json') - ]; - - let configData = null; - let configPath = null; - - // Try to read from either config file - for (const filepath of configPaths) { - try { - const fileContent = await fs.readFile(filepath, 'utf8'); - configData = JSON.parse(fileContent); - configPath = filepath; - console.log(`✅ Found Claude config at: ${filepath}`); - break; - } catch (error) { - // File doesn't exist or is not valid JSON, try next - console.log(`ℹ️ Config not found or invalid at: ${filepath}`); - } - } - - if (!configData) { - return res.json({ - success: false, - message: 'No Claude configuration file found', - servers: [] - }); - } - - // Extract MCP servers from the config - const servers = []; - - // Check for user-scoped MCP servers (at root level) - if (configData.mcpServers && typeof configData.mcpServers === 'object' && Object.keys(configData.mcpServers).length > 0) { - console.log('🔍 Found user-scoped MCP servers:', Object.keys(configData.mcpServers)); - for (const [name, config] of Object.entries(configData.mcpServers)) { - const server = { - id: name, - name: name, - type: 'stdio', // Default type - scope: 'user', // User scope - available across all projects - config: {}, - raw: config // Include raw config for full details - }; - - // Determine transport type and extract config - if (config.command) { - server.type = 'stdio'; - server.config.command = config.command; - server.config.args = config.args || []; - server.config.env = config.env || {}; - } else if (config.url) { - server.type = config.transport || 'http'; - server.config.url = config.url; - server.config.headers = config.headers || {}; - } - - servers.push(server); - } - } - - // Check for local-scoped MCP servers (project-specific) - const currentProjectPath = process.cwd(); - - // Check under 'projects' key - if (configData.projects && configData.projects[currentProjectPath]) { - const projectConfig = configData.projects[currentProjectPath]; - if (projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object' && Object.keys(projectConfig.mcpServers).length > 0) { - console.log(`🔍 Found local-scoped MCP servers for ${currentProjectPath}:`, Object.keys(projectConfig.mcpServers)); - for (const [name, config] of Object.entries(projectConfig.mcpServers)) { - const server = { - id: `local:${name}`, // Prefix with scope for uniqueness - name: name, // Keep original name - type: 'stdio', // Default type - scope: 'local', // Local scope - only for this project - projectPath: currentProjectPath, - config: {}, - raw: config // Include raw config for full details - }; - - // Determine transport type and extract config - if (config.command) { - server.type = 'stdio'; - server.config.command = config.command; - server.config.args = config.args || []; - server.config.env = config.env || {}; - } else if (config.url) { - server.type = config.transport || 'http'; - server.config.url = config.url; - server.config.headers = config.headers || {}; - } - - servers.push(server); - } - } - } - - console.log(`📋 Found ${servers.length} MCP servers in config`); - - res.json({ - success: true, - configPath: configPath, - servers: servers - }); - } catch (error) { - console.error('Error reading Claude config:', error); - res.status(500).json({ - error: 'Failed to read Claude configuration', - details: error.message - }); - } -}); - -// Helper functions to parse Claude CLI output -function parseClaudeListOutput(output) { - const servers = []; - const lines = output.split('\n').filter(line => line.trim()); - - for (const line of lines) { - // Skip the header line - if (line.includes('Checking MCP server health')) continue; - - // Parse lines like "test: test test - ✗ Failed to connect" - // or "server-name: command or description - ✓ Connected" - if (line.includes(':')) { - const colonIndex = line.indexOf(':'); - const name = line.substring(0, colonIndex).trim(); - - // Skip empty names - if (!name) continue; - - // Extract the rest after the name - const rest = line.substring(colonIndex + 1).trim(); - - // Try to extract description and status - let description = rest; - let status = 'unknown'; - let type = 'stdio'; // default type - - // Check for status indicators - if (rest.includes('✓') || rest.includes('✗')) { - const statusMatch = rest.match(/(.*?)\s*-\s*([✓✗].*)$/); - if (statusMatch) { - description = statusMatch[1].trim(); - status = statusMatch[2].includes('✓') ? 'connected' : 'failed'; - } - } - - // Try to determine type from description - if (description.startsWith('http://') || description.startsWith('https://')) { - type = 'http'; - } - - servers.push({ - name, - type, - status: status || 'active', - description - }); - } - } - - console.log('🔍 Parsed Claude CLI servers:', servers); - return servers; -} - -function parseClaudeGetOutput(output) { - // Parse the output from 'claude mcp get ' 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; \ No newline at end of file diff --git a/server/routes/taskmaster.js b/server/routes/taskmaster.js index 632d99d5..54f7153a 100644 --- a/server/routes/taskmaster.js +++ b/server/routes/taskmaster.js @@ -13,16 +13,10 @@ import fs from 'fs'; import path from 'path'; import { promises as fsPromises } from 'fs'; import { spawn } from 'child_process'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import os from 'os'; import { extractProjectDirectory } from '../projects.js'; import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js'; import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - const router = express.Router(); /** @@ -100,140 +94,6 @@ async function checkTaskMasterInstallation() { }); } -/** - * Detect .taskmaster folder presence in a given project directory - * @param {string} projectPath - Absolute path to project directory - * @returns {Promise} Detection result with status and metadata - */ -async function detectTaskMasterFolder(projectPath) { - try { - const taskMasterPath = path.join(projectPath, '.taskmaster'); - - // Check if .taskmaster directory exists - try { - const stats = await fsPromises.stat(taskMasterPath); - if (!stats.isDirectory()) { - return { - hasTaskmaster: false, - reason: '.taskmaster exists but is not a directory' - }; - } - } catch (error) { - if (error.code === 'ENOENT') { - return { - hasTaskmaster: false, - reason: '.taskmaster directory not found' - }; - } - throw error; - } - - // Check for key TaskMaster files - const keyFiles = [ - 'tasks/tasks.json', - 'config.json' - ]; - - const fileStatus = {}; - let hasEssentialFiles = true; - - for (const file of keyFiles) { - const filePath = path.join(taskMasterPath, file); - try { - await fsPromises.access(filePath, fs.constants.R_OK); - fileStatus[file] = true; - } catch (error) { - fileStatus[file] = false; - if (file === 'tasks/tasks.json') { - hasEssentialFiles = false; - } - } - } - - // Parse tasks.json if it exists for metadata - let taskMetadata = null; - if (fileStatus['tasks/tasks.json']) { - try { - const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json'); - const tasksContent = await fsPromises.readFile(tasksPath, 'utf8'); - const tasksData = JSON.parse(tasksContent); - - // Handle both tagged and legacy formats - let tasks = []; - if (tasksData.tasks) { - // Legacy format - tasks = tasksData.tasks; - } else { - // Tagged format - get tasks from all tags - Object.values(tasksData).forEach(tagData => { - if (tagData.tasks) { - tasks = tasks.concat(tagData.tasks); - } - }); - } - - // Calculate task statistics - const stats = tasks.reduce((acc, task) => { - acc.total++; - acc[task.status] = (acc[task.status] || 0) + 1; - - // Count subtasks - if (task.subtasks) { - task.subtasks.forEach(subtask => { - acc.subtotalTasks++; - acc.subtasks = acc.subtasks || {}; - acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1; - }); - } - - return acc; - }, { - total: 0, - subtotalTasks: 0, - pending: 0, - 'in-progress': 0, - done: 0, - review: 0, - deferred: 0, - cancelled: 0, - subtasks: {} - }); - - taskMetadata = { - taskCount: stats.total, - subtaskCount: stats.subtotalTasks, - completed: stats.done || 0, - pending: stats.pending || 0, - inProgress: stats['in-progress'] || 0, - review: stats.review || 0, - completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0, - lastModified: (await fsPromises.stat(tasksPath)).mtime.toISOString() - }; - } catch (parseError) { - console.warn('Failed to parse tasks.json:', parseError.message); - taskMetadata = { error: 'Failed to parse tasks.json' }; - } - } - - return { - hasTaskmaster: true, - hasEssentialFiles, - files: fileStatus, - metadata: taskMetadata, - path: taskMasterPath - }; - - } catch (error) { - console.error('Error detecting TaskMaster folder:', error); - return { - hasTaskmaster: false, - reason: `Error checking directory: ${error.message}` - }; - } -} - -// MCP detection is now handled by the centralized utility - // API Routes /** @@ -271,298 +131,6 @@ router.get('/installation-status', async (req, res) => { } }); -/** - * GET /api/taskmaster/detect/:projectName - * Detect TaskMaster configuration for a specific project - */ -router.get('/detect/:projectName', async (req, res) => { - try { - const { projectName } = req.params; - - // Use the existing extractProjectDirectory function to get actual project path - let projectPath; - try { - projectPath = await extractProjectDirectory(projectName); - } catch (error) { - console.error('Error extracting project directory:', error); - return res.status(404).json({ - error: 'Project path not found', - projectName, - message: error.message - }); - } - - // Verify the project path exists - try { - await fsPromises.access(projectPath, fs.constants.R_OK); - } catch (error) { - return res.status(404).json({ - error: 'Project path not accessible', - projectPath, - projectName, - message: error.message - }); - } - - // Run detection in parallel - const [taskMasterResult, mcpResult] = await Promise.all([ - detectTaskMasterFolder(projectPath), - detectTaskMasterMCPServer() - ]); - - // Determine overall status - let status = 'not-configured'; - if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) { - if (mcpResult.hasMCPServer && mcpResult.isConfigured) { - status = 'fully-configured'; - } else { - status = 'taskmaster-only'; - } - } else if (mcpResult.hasMCPServer && mcpResult.isConfigured) { - status = 'mcp-only'; - } - - const responseData = { - projectName, - projectPath, - status, - taskmaster: taskMasterResult, - mcp: mcpResult, - timestamp: new Date().toISOString() - }; - - res.json(responseData); - - } catch (error) { - console.error('TaskMaster detection error:', error); - res.status(500).json({ - error: 'Failed to detect TaskMaster configuration', - message: error.message - }); - } -}); - -/** - * GET /api/taskmaster/detect-all - * Detect TaskMaster configuration for all known projects - * This endpoint works with the existing projects system - */ -router.get('/detect-all', async (req, res) => { - try { - // Import getProjects from the projects module - const { getProjects } = await import('../projects.js'); - const projects = await getProjects(); - - // Run detection for all projects in parallel - const detectionPromises = projects.map(async (project) => { - try { - // Use the project's fullPath if available, otherwise extract the directory - let projectPath; - if (project.fullPath) { - projectPath = project.fullPath; - } else { - try { - projectPath = await extractProjectDirectory(project.name); - } catch (error) { - throw new Error(`Failed to extract project directory: ${error.message}`); - } - } - - const [taskMasterResult, mcpResult] = await Promise.all([ - detectTaskMasterFolder(projectPath), - detectTaskMasterMCPServer() - ]); - - // Determine status - let status = 'not-configured'; - if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) { - if (mcpResult.hasMCPServer && mcpResult.isConfigured) { - status = 'fully-configured'; - } else { - status = 'taskmaster-only'; - } - } else if (mcpResult.hasMCPServer && mcpResult.isConfigured) { - status = 'mcp-only'; - } - - return { - projectName: project.name, - displayName: project.displayName, - projectPath, - status, - taskmaster: taskMasterResult, - mcp: mcpResult - }; - } catch (error) { - return { - projectName: project.name, - displayName: project.displayName, - status: 'error', - error: error.message - }; - } - }); - - const results = await Promise.all(detectionPromises); - - res.json({ - projects: results, - summary: { - total: results.length, - fullyConfigured: results.filter(p => p.status === 'fully-configured').length, - taskmasterOnly: results.filter(p => p.status === 'taskmaster-only').length, - mcpOnly: results.filter(p => p.status === 'mcp-only').length, - notConfigured: results.filter(p => p.status === 'not-configured').length, - errors: results.filter(p => p.status === 'error').length - }, - timestamp: new Date().toISOString() - }); - - } catch (error) { - console.error('Bulk TaskMaster detection error:', error); - res.status(500).json({ - error: 'Failed to detect TaskMaster configuration for projects', - message: error.message - }); - } -}); - -/** - * POST /api/taskmaster/initialize/:projectName - * Initialize TaskMaster in a project (placeholder for future CLI integration) - */ -router.post('/initialize/:projectName', async (req, res) => { - try { - const { projectName } = req.params; - const { rules } = req.body; // Optional rule profiles - - // This will be implemented in a later subtask with CLI integration - res.status(501).json({ - error: 'TaskMaster initialization not yet implemented', - message: 'This endpoint will execute task-master init via CLI in a future update', - projectName, - rules - }); - - } catch (error) { - console.error('TaskMaster initialization error:', error); - res.status(500).json({ - error: 'Failed to initialize TaskMaster', - message: error.message - }); - } -}); - -/** - * GET /api/taskmaster/next/:projectName - * Get the next recommended task using task-master CLI - */ -router.get('/next/:projectName', async (req, res) => { - try { - const { projectName } = req.params; - - // Get project path - let projectPath; - try { - projectPath = await extractProjectDirectory(projectName); - } catch (error) { - return res.status(404).json({ - error: 'Project not found', - message: `Project "${projectName}" does not exist` - }); - } - - // Try to execute task-master next command - try { - const { spawn } = await import('child_process'); - - const nextTaskCommand = spawn('task-master', ['next'], { - cwd: projectPath, - stdio: ['pipe', 'pipe', 'pipe'] - }); - - let stdout = ''; - let stderr = ''; - - nextTaskCommand.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - nextTaskCommand.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - await new Promise((resolve, reject) => { - nextTaskCommand.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`task-master next failed with code ${code}: ${stderr}`)); - } - }); - - nextTaskCommand.on('error', (error) => { - reject(error); - }); - }); - - // Parse the output - task-master next usually returns JSON - let nextTaskData = null; - if (stdout.trim()) { - try { - nextTaskData = JSON.parse(stdout); - } catch (parseError) { - // If not JSON, treat as plain text - nextTaskData = { message: stdout.trim() }; - } - } - - res.json({ - projectName, - projectPath, - nextTask: nextTaskData, - timestamp: new Date().toISOString() - }); - - } catch (cliError) { - console.warn('Failed to execute task-master CLI:', cliError.message); - - // Fallback to loading tasks and finding next one locally - // Use localhost to bypass proxy for internal server-to-server calls - const tasksResponse = await fetch(`http://localhost:${process.env.SERVER_PORT || process.env.PORT || '3001'}/api/taskmaster/tasks/${encodeURIComponent(projectName)}`, { - headers: { - 'Authorization': req.headers.authorization - } - }); - - if (tasksResponse.ok) { - const tasksData = await tasksResponse.json(); - const nextTask = tasksData.tasks?.find(task => - task.status === 'pending' || task.status === 'in-progress' - ) || null; - - res.json({ - projectName, - projectPath, - nextTask, - fallback: true, - message: 'Used fallback method (CLI not available)', - timestamp: new Date().toISOString() - }); - } else { - throw new Error('Failed to load tasks via fallback method'); - } - } - - } catch (error) { - console.error('TaskMaster next task error:', error); - res.status(500).json({ - error: 'Failed to get next task', - message: error.message - }); - } -}); - /** * GET /api/taskmaster/tasks/:projectName * Load actual tasks from .taskmaster/tasks/tasks.json @@ -904,66 +472,6 @@ router.get('/prd/:projectName/:fileName', async (req, res) => { } }); -/** - * DELETE /api/taskmaster/prd/:projectName/:fileName - * Delete a specific PRD file - */ -router.delete('/prd/:projectName/:fileName', async (req, res) => { - try { - const { projectName, fileName } = req.params; - - // Get project path - let projectPath; - try { - projectPath = await extractProjectDirectory(projectName); - } catch (error) { - return res.status(404).json({ - error: 'Project not found', - message: `Project "${projectName}" does not exist` - }); - } - - const filePath = path.join(projectPath, '.taskmaster', 'docs', fileName); - - // Check if file exists - try { - await fsPromises.access(filePath, fs.constants.F_OK); - } catch (error) { - return res.status(404).json({ - error: 'PRD file not found', - message: `File "${fileName}" does not exist` - }); - } - - // Delete the file - try { - await fsPromises.unlink(filePath); - - res.json({ - projectName, - projectPath, - fileName, - message: 'PRD file deleted successfully', - timestamp: new Date().toISOString() - }); - - } catch (deleteError) { - console.error('Failed to delete PRD file:', deleteError); - return res.status(500).json({ - error: 'Failed to delete PRD file', - message: deleteError.message - }); - } - - } catch (error) { - console.error('PRD delete error:', error); - res.status(500).json({ - error: 'Failed to delete PRD file', - message: error.message - }); - } -}); - /** * POST /api/taskmaster/init/:projectName * Initialize TaskMaster in a project diff --git a/server/shared/interfaces.ts b/server/shared/interfaces.ts index e095b0a8..432a5a8f 100644 --- a/server/shared/interfaces.ts +++ b/server/shared/interfaces.ts @@ -3,7 +3,6 @@ import type { FetchHistoryResult, LLMProvider, McpScope, - McpTransport, NormalizedMessage, ProviderAuthStatus, ProviderMcpServer, @@ -46,15 +45,4 @@ export interface IProviderMcp { removeServer( input: { name: string; scope?: McpScope; workspacePath?: string }, ): Promise<{ removed: boolean; provider: LLMProvider; name: string; scope: McpScope }>; - runServer( - input: { name: string; scope?: McpScope; workspacePath?: string }, - ): Promise<{ - provider: LLMProvider; - name: string; - scope: McpScope; - transport: McpTransport; - reachable: boolean; - statusCode?: number; - error?: string; - }>; } diff --git a/server/utils/mcp-detector.js b/server/utils/mcp-detector.js index 4439353f..0d9241ae 100644 --- a/server/utils/mcp-detector.js +++ b/server/utils/mcp-detector.js @@ -145,54 +145,3 @@ export async function detectTaskMasterMCPServer() { } } -/** - * Get all configured MCP servers (not just TaskMaster) - * @returns {Promise} All MCP servers configuration - */ -export async function getAllMCPServers() { - try { - const homeDir = os.homedir(); - const configPaths = [ - path.join(homeDir, '.claude.json'), - path.join(homeDir, '.claude', 'settings.json') - ]; - - let configData = null; - let configPath = null; - - // Try to read from either config file - for (const filepath of configPaths) { - try { - const fileContent = await fsPromises.readFile(filepath, 'utf8'); - configData = JSON.parse(fileContent); - configPath = filepath; - break; - } catch (error) { - continue; - } - } - - if (!configData) { - return { - hasConfig: false, - servers: {}, - projectServers: {} - }; - } - - return { - hasConfig: true, - configPath, - servers: configData.mcpServers || {}, - projectServers: configData.projects || {} - }; - } catch (error) { - console.error('Error getting all MCP servers:', error); - return { - hasConfig: false, - error: error.message, - servers: {}, - projectServers: {} - }; - } -} \ No newline at end of file diff --git a/src/utils/api.js b/src/utils/api.js index 438cab82..09158318 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -99,11 +99,6 @@ export const api = { if (token) params.set('token', token); return `/api/search/conversations?${params.toString()}`; }, - createProject: (path) => - authenticatedFetch('/api/projects/create', { - method: 'POST', - body: JSON.stringify({ path }), - }), createWorkspace: (workspaceData) => authenticatedFetch('/api/projects/create-workspace', { method: 'POST',