diff --git a/gitnexus-mcp/package.json b/gitnexus-mcp/package.json index e6d0ffa07..2bd505224 100644 --- a/gitnexus-mcp/package.json +++ b/gitnexus-mcp/package.json @@ -16,10 +16,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", - "commander": "^12.0.0", - "ws": "^8.16.0", - "chalk": "^5.3.0", - "ora": "^8.0.0" + "ws": "^8.16.0" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/gitnexus-mcp/src/bridge/daemon-client.ts b/gitnexus-mcp/src/bridge/daemon-client.ts deleted file mode 100644 index 4f5c5d4c1..000000000 --- a/gitnexus-mcp/src/bridge/daemon-client.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Daemon Client - * - * WebSocket client that connects to the GitNexus daemon. - * Used by `serve` command to route tool calls through the daemon to the browser. - * Also receives codebase context for MCP resource exposure. - */ - -import WebSocket from 'ws'; - -export interface BridgeMessage { - id: string; - method?: string; - params?: any; - result?: any; - error?: { message: string }; - type?: string; - context?: CodebaseContext; - agentName?: string; -} - -/** - * Codebase context from browser - */ -export interface CodebaseContext { - projectName: string; - stats: { - fileCount: number; - functionCount: number; - classCount: number; - interfaceCount: number; - methodCount: number; - }; - hotspots: Array<{ - name: string; - type: string; - filePath: string; - connections: number; - }>; - folderTree: string; -} - -type RequestResolver = { - resolve: (result: any) => void; - reject: (error: Error) => void; -}; - -export class DaemonClient { - private ws: WebSocket | null = null; - private pendingRequests: Map = new Map(); - private requestId = 0; - private _context: CodebaseContext | null = null; - private contextListeners: Set<(context: CodebaseContext | null) => void> = new Set(); - private agentName: string; - - constructor(private port: number = 54319, agentName?: string) { - // Detect agent from environment or use provided name - this.agentName = agentName || process.env.GITNEXUS_AGENT || this.detectAgent(); - } - - private detectAgent(): string { - // Try to detect agent from environment clues - if (process.env.CURSOR_SESSION_ID) return 'Cursor'; - if (process.env.CLAUDE_CODE) return 'Claude Code'; - if (process.env.WINDSURF_SESSION) return 'Windsurf'; - return 'Unknown'; - } - - /** - * Connect to the daemon - */ - async connect(): Promise { - return new Promise((resolve, reject) => { - // Connect to /mcp path so daemon knows this is an MCP client - this.ws = new WebSocket(`ws://localhost:${this.port}/mcp`); - - this.ws.on('open', () => { - resolve(); - }); - - this.ws.on('error', (error) => { - reject(error); - }); - - this.ws.on('message', (data) => { - try { - const msg: BridgeMessage = JSON.parse(data.toString()); - this.handleMessage(msg); - } catch (error) { - console.error('Failed to parse message:', error); - } - }); - - this.ws.on('close', () => { - this.ws = null; - this._context = null; - // Reject all pending requests - for (const { reject } of this.pendingRequests.values()) { - reject(new Error('Connection closed')); - } - this.pendingRequests.clear(); - }); - }); - } - - private handleMessage(msg: BridgeMessage) { - // Handle context update from daemon - if (msg.type === 'context_update' && msg.context) { - this._context = msg.context; - this.notifyContextListeners(); - return; - } - - // Response from daemon (originally from browser) - if (msg.id && this.pendingRequests.has(msg.id)) { - const { resolve, reject } = this.pendingRequests.get(msg.id)!; - this.pendingRequests.delete(msg.id); - - if (msg.error) { - reject(new Error(msg.error.message)); - } else { - resolve(msg.result); - } - } - } - - /** - * Get current codebase context - */ - get context(): CodebaseContext | null { - return this._context; - } - - /** - * Listen for context changes - */ - onContextChange(listener: (context: CodebaseContext | null) => void) { - this.contextListeners.add(listener); - return () => this.contextListeners.delete(listener); - } - - private notifyContextListeners() { - this.contextListeners.forEach(listener => listener(this._context)); - } - - /** - * Check if connected to daemon - */ - get isConnected(): boolean { - return this.ws !== null && this.ws.readyState === WebSocket.OPEN; - } - - /** - * Call a tool (routed through daemon to browser) - */ - async callTool(method: string, params: any): Promise { - if (!this.isConnected) { - throw new Error('Not connected to daemon'); - } - - const id = `req_${++this.requestId}`; - - return new Promise((resolve, reject) => { - this.pendingRequests.set(id, { resolve, reject }); - - const msg: BridgeMessage = { id, method, params, agentName: this.agentName }; - this.ws!.send(JSON.stringify(msg)); - - // Timeout after 30 seconds - setTimeout(() => { - if (this.pendingRequests.has(id)) { - this.pendingRequests.delete(id); - reject(new Error('Request timeout')); - } - }, 30000); - }); - } - - /** - * Disconnect from daemon - */ - disconnect() { - this.ws?.close(); - this.ws = null; - } -} diff --git a/gitnexus-mcp/src/bridge/websocket-server.ts b/gitnexus-mcp/src/bridge/websocket-server.ts index 0d43fb7d5..503f5e209 100644 --- a/gitnexus-mcp/src/bridge/websocket-server.ts +++ b/gitnexus-mcp/src/bridge/websocket-server.ts @@ -14,6 +14,29 @@ export interface BridgeMessage { params?: any; result?: any; error?: { message: string }; + type?: 'context' | string; + agentName?: string; +} + +/** + * Codebase context sent from the GitNexus browser app + */ +export interface CodebaseContext { + projectName: string; + stats: { + fileCount: number; + functionCount: number; + classCount: number; + interfaceCount: number; + methodCount: number; + }; + hotspots: Array<{ + name: string; + type: string; + filePath: string; + connections: number; + }>; + folderTree: string; } type RequestResolver = { @@ -42,8 +65,21 @@ export class WebSocketBridge { private pendingRequests: Map = new Map(); private requestId = 0; private started = false; + private _context: CodebaseContext | null = null; + private contextListeners: Set<(context: CodebaseContext | null) => void> = new Set(); + private agentName: string; - constructor(private port: number = 54319) {} + constructor(private port: number = 54319, agentName?: string) { + this.agentName = agentName || process.env.GITNEXUS_AGENT || this.detectAgent(); + } + + private detectAgent(): string { + // Try to detect agent from environment clues + if (process.env.CURSOR_SESSION_ID) return 'Cursor'; + if (process.env.CLAUDE_CODE) return 'Claude Code'; + if (process.env.WINDSURF_SESSION) return 'Windsurf'; + return 'Unknown'; + } /** * Start the WebSocket server (handles port-in-use gracefully) @@ -67,6 +103,9 @@ export class WebSocketBridge { this.client.close(); } this.client = ws; + // Clear context until browser sends an update + this._context = null; + this.notifyContextListeners(); ws.on('message', (data) => { try { @@ -80,6 +119,8 @@ export class WebSocketBridge { ws.on('close', () => { if (this.client === ws) { this.client = null; + this._context = null; + this.notifyContextListeners(); } }); @@ -101,6 +142,13 @@ export class WebSocketBridge { } private handleMessage(msg: BridgeMessage) { + // Browser can proactively send codebase context + if (msg.type === 'context' && msg.params) { + this._context = msg.params as CodebaseContext; + this.notifyContextListeners(); + return; + } + // This is a response to a pending request if (msg.id && this.pendingRequests.has(msg.id)) { const { resolve, reject } = this.pendingRequests.get(msg.id)!; @@ -121,6 +169,25 @@ export class WebSocketBridge { return this.client !== null && this.client.readyState === WebSocket.OPEN; } + /** + * Latest context received from browser (if any) + */ + get context(): CodebaseContext | null { + return this._context; + } + + /** + * Listen for context changes + */ + onContextChange(listener: (context: CodebaseContext | null) => void) { + this.contextListeners.add(listener); + return () => this.contextListeners.delete(listener); + } + + private notifyContextListeners() { + this.contextListeners.forEach((listener) => listener(this._context)); + } + /** * Check if server started successfully */ @@ -141,7 +208,7 @@ export class WebSocketBridge { return new Promise((resolve, reject) => { this.pendingRequests.set(id, { resolve, reject }); - const msg: BridgeMessage = { id, method, params }; + const msg: BridgeMessage = { id, method, params, agentName: this.agentName }; this.client!.send(JSON.stringify(msg)); // Timeout after 30 seconds @@ -160,4 +227,11 @@ export class WebSocketBridge { close() { this.wss?.close(); } + + /** + * MCP server calls this on shutdown + */ + disconnect() { + this.close(); + } } diff --git a/gitnexus-mcp/src/cli.ts b/gitnexus-mcp/src/cli.ts index 726b44ed1..281ecb0fb 100644 --- a/gitnexus-mcp/src/cli.ts +++ b/gitnexus-mcp/src/cli.ts @@ -6,42 +6,55 @@ * and GitNexus code intelligence running in the browser. */ -import { Command } from 'commander'; -import { setupCommand } from './commands/setup.js'; import { serveCommand } from './commands/serve.js'; -import { daemonCommand } from './commands/daemon.js'; -import { generateGuidanceCommand } from './commands/generate-guidance.js'; +/** + * Minimal CLI: + * - Default: start MCP stdio server + local browser WebSocket bridge + * - Optional: `serve` alias, and `--port ` + * + * This is designed for MCP clients (Cursor/Claude/Windsurf) which spawn this + * process automatically; users should not need to run commands manually. + */ -const program = new Command(); +function parsePort(argv: string[]): string { + const portFlagIndex = argv.findIndex((a) => a === '--port' || a === '-p'); + if (portFlagIndex !== -1) { + const value = argv[portFlagIndex + 1]; + if (value) return value; + } + // Support `--port=54319` + const portEq = argv.find((a) => a.startsWith('--port=')); + if (portEq) return portEq.split('=')[1] || '54319'; + return '54319'; +} -program - .name('gitnexus-mcp') - .description('MCP bridge for GitNexus code intelligence') - .version('0.1.0'); +async function main() { + const argv = process.argv.slice(2); + const first = argv[0]; + const port = parsePort(argv); -program - .command('setup') - .description('Detect AI tools and configure MCP settings') - .action(setupCommand); + // Allow `gitnexus-mcp serve` for compatibility, but default to serve anyway + if (!first || first === 'serve') { + await serveCommand({ port }); + return; + } -program - .command('daemon') - .description('Start the MCP daemon (run this once in background)') - .option('-p, --port ', 'WebSocket port', '54319') - .action(daemonCommand); - -program - .command('serve') - .description('Start MCP server for an AI tool (connects to daemon)') - .option('-p, --port ', 'Daemon port to connect to', '54319') - .action(serveCommand); - -program - .command('generate-guidance') - .description('Generate AI assistant guidance files (.cursorrules, AGENTS.md)') - .option('-o, --output ', 'Output directory', '.') - .action(generateGuidanceCommand); - -program.parse(); + // Minimal help for unknown commands + if (first === '--help' || first === '-h') { + // eslint-disable-next-line no-console + console.log('gitnexus-mcp\n\nUsage:\n gitnexus-mcp [serve] [--port ]\n'); + process.exit(0); + } + // eslint-disable-next-line no-console + console.error(`Unknown command: ${first}`); + // eslint-disable-next-line no-console + console.error('Usage: gitnexus-mcp [serve] [--port ]'); + process.exit(1); +} +main().catch((err) => { + // eslint-disable-next-line no-console + console.error(err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/gitnexus-mcp/src/commands/daemon.ts b/gitnexus-mcp/src/commands/daemon.ts deleted file mode 100644 index 5c7995515..000000000 --- a/gitnexus-mcp/src/commands/daemon.ts +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Daemon Command - * - * Runs a persistent daemon that acts as the central hub for: - * - Browser connections (GitNexus web app) - * - MCP server connections (from AI tools like Cursor, Antigravity) - * - * This allows multiple AI tools to share the same browser bridge. - * Also stores codebase context sent by the browser for MCP resource exposure. - */ - -import { WebSocketServer, WebSocket } from 'ws'; - -interface BridgeMessage { - id: string; - method?: string; - params?: any; - result?: any; - error?: { message: string }; - source?: 'browser' | 'mcp'; - type?: 'context' | 'tool_call' | 'tool_result'; - agentName?: string; -} - -/** - * Codebase context sent from browser - */ -interface CodebaseContext { - projectName: string; - stats: { - fileCount: number; - functionCount: number; - classCount: number; - interfaceCount: number; - methodCount: number; - }; - hotspots: Array<{ - name: string; - type: string; - filePath: string; - connections: number; - }>; - folderTree: string; -} - -interface DaemonOptions { - port: string; -} - -// Module-level state for context (so MCP servers can access it) -let currentContext: CodebaseContext | null = null; - -export function getCodebaseContext(): CodebaseContext | null { - return currentContext; -} - -export async function daemonCommand(options: DaemonOptions) { - const port = parseInt(options.port, 10); - - let browserClient: WebSocket | null = null; - const mcpClients: Set = new Set(); - const pendingRequests: Map = new Map(); // request ID → which MCP client sent it - - // Track agent sessions for multi-instance support - interface AgentSession { - agentType: string; // e.g., "Cursor", "Claude" - instanceId: number; // e.g., 1, 2, 3 for multiple instances - sessionId: string; // unique ID - } - const agentSessions: Map = new Map(); - const agentInstanceCounters: Map = new Map(); // agentType → next instance number - - const wss = new WebSocketServer({ port }); - - console.log(`šŸ”Œ GitNexus MCP Daemon running on port ${port}`); - console.log(' Waiting for connections...\n'); - - wss.on('connection', (ws, req) => { - // Determine client type from URL path - const url = new URL(req.url || '/', `http://localhost:${port}`); - const clientType = url.pathname === '/mcp' ? 'mcp' : 'browser'; - - if (clientType === 'browser') { - // Browser connection - if (browserClient) { - browserClient.close(); - } - browserClient = ws; - console.log('āœ… Browser connected'); - - ws.on('message', (data) => { - try { - const msg: BridgeMessage = JSON.parse(data.toString()); - - // Handle context update from browser - if (msg.type === 'context' && msg.params) { - currentContext = msg.params as CodebaseContext; - console.log(`šŸ“Š Context received: ${currentContext.projectName} (${currentContext.stats.fileCount} files)`); - - // Broadcast context update to all MCP clients - const contextNotification = JSON.stringify({ - type: 'context_update', - context: currentContext, - }); - mcpClients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(contextNotification); - } - }); - return; - } - - // Response from browser - route back to the MCP client that sent the request - if (msg.id && pendingRequests.has(msg.id)) { - const mcpClient = pendingRequests.get(msg.id)!; - pendingRequests.delete(msg.id); - if (mcpClient.readyState === WebSocket.OPEN) { - mcpClient.send(JSON.stringify(msg)); - } - } - } catch (error) { - console.error('Failed to parse browser message:', error); - } - }); - - ws.on('close', () => { - if (browserClient === ws) { - browserClient = null; - currentContext = null; // Clear context when browser disconnects - console.log('āŒ Browser disconnected'); - } - }); - - } else { - // MCP server connection - mcpClients.add(ws); - console.log(`āœ… MCP client connected (total: ${mcpClients.size})`); - - // Send current context to new MCP client if available - if (currentContext) { - ws.send(JSON.stringify({ - type: 'context_update', - context: currentContext, - })); - } - - ws.on('message', (data) => { - try { - const msg: BridgeMessage = JSON.parse(data.toString()); - - // Handle context request - if (msg.method === 'getContext') { - ws.send(JSON.stringify({ - id: msg.id, - result: currentContext, - })); - return; - } - - // Tool call request from MCP server - forward to browser - if (msg.method && msg.id) { - // Handle session tracking - let session = agentSessions.get(ws); - const rawAgentName = msg.agentName || 'Unknown'; - - if (!session) { - // First time seeing this connection - assign instance ID - const currentCount = agentInstanceCounters.get(rawAgentName) || 0; - const newCount = currentCount + 1; - agentInstanceCounters.set(rawAgentName, newCount); - - session = { - agentType: rawAgentName, - instanceId: newCount, - sessionId: `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` - }; - agentSessions.set(ws, session); - console.log(`šŸ†• Registered ${rawAgentName} #${newCount}`); - - // Notify browser about new agent (optional, mainly for UI updates if needed later) - // browserClient?.send(JSON.stringify({ type: 'agent_info', ... })); - } - - // Construct display name (e.g., "Claude #1") - // If it's the first instance, maybe just keep "Claude" or do we always want numbers? - // User asked for tracking multiple, so "Claude #1" is clearer even for the first if we expect multiples. - // Let's use format "Name #N" - const uniqueAgentName = `${session.agentType} #${session.instanceId}`; - - if (browserClient && browserClient.readyState === WebSocket.OPEN) { - pendingRequests.set(msg.id, ws); - // Forward with unique name - const forwardMsg = { ...msg, agentName: uniqueAgentName }; - browserClient.send(JSON.stringify(forwardMsg)); - } else { - // No browser connected - ws.send(JSON.stringify({ - id: msg.id, - error: { message: 'GitNexus browser not connected. Open GitNexus and enable MCP toggle.' } - })); - } - } - } catch (error) { - console.error('Failed to parse MCP message:', error); - } - }); - - ws.on('close', () => { - const session = agentSessions.get(ws); - if (session) { - console.log(`āŒ ${session.agentType} #${session.instanceId} disconnected`); - agentSessions.delete(ws); - } - mcpClients.delete(ws); - console.log(`Remaining clients: ${mcpClients.size}`); - }); - } - - ws.on('error', (error) => { - console.error('WebSocket error:', error.message); - }); - }); - - wss.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EADDRINUSE') { - console.error(`āŒ Port ${port} is already in use.`); - console.error(' A daemon may already be running. Check with: lsof -i :54319'); - process.exit(1); - } - console.error('Server error:', error); - }); - - // Keep the daemon running - process.on('SIGINT', () => { - console.log('\nšŸ‘‹ Shutting down daemon...'); - wss.close(); - process.exit(0); - }); -} diff --git a/gitnexus-mcp/src/commands/generate-guidance.ts b/gitnexus-mcp/src/commands/generate-guidance.ts deleted file mode 100644 index fe6f278a0..000000000 --- a/gitnexus-mcp/src/commands/generate-guidance.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Generate Guidance Command - * - * Creates AI assistant guidance files for a project: - * - .cursor/rules/gitnexus.mdc - Cursor skills file - * - AGENTS.md - Generic tool usage patterns for Claude Code etc. - */ - -import { writeFile, mkdir } from 'fs/promises'; -import { existsSync } from 'fs'; -import { join } from 'path'; -import chalk from 'chalk'; - -interface GenerateOptions { - output: string; -} - -// Cursor uses MDC format (Markdown with frontmatter) -const CURSOR_RULES_CONTENT = `--- -description: GitNexus MCP code intelligence tools -globs: -alwaysApply: true ---- - -# GitNexus Code Intelligence - -This project uses GitNexus MCP for code understanding. You have access to these tools: - -## Available MCP Tools - -### search -Semantic search across code. Returns functions, classes, files matching meaning. -- Use for: Finding code by description, not exact text -- Example: "authentication logic", "database connection handling" - -### cypher -Direct KuzuDB graph queries for relationship traversal. -- Pattern: \`MATCH (f:Function)-[:CodeRelation {type:'CALLS'}]->(g:Function) RETURN f.name, g.name\` -- Node types: File, Folder, Function, Class, Interface, Method, CodeElement -- Relation types: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS - -### blastRadius -Find all code affected by changing a node. Returns N-hop connections. -- Use for: Before refactoring, understanding impact of changes - -### highlight -Highlight nodes in the graph visualization. - -### context -Get project overview: stats, hotspots, folder tree. -- Use for: Understanding project structure - -### grep -Regex pattern search across file contents. -- Use for: Finding exact patterns, TODOs - -### read -Read file content by path. Supports line ranges. - -## Best Practices - -1. **Start with context** - Understand project structure first -2. **Use search for discovery** - Find by meaning, not keywords -3. **Use cypher for relationships** - Call chains, imports, inheritance -4. **Check blast radius before refactoring** - Understand impact - -## Cypher Examples - -\`\`\`cypher -# Find functions that call a specific function -MATCH (caller:Function)-[:CodeRelation {type:'CALLS'}]->(target:Function {name: 'authenticate'}) -RETURN caller.name, caller.filePath - -# Find class inheritance -MATCH (child:Class)-[:CodeRelation {type:'EXTENDS'}]->(parent:Class) -RETURN child.name, parent.name -\`\`\` -`; - -const AGENTS_MD_CONTENT = `# GitNexus Agent Guidelines - -This document describes how to use GitNexus MCP tools for code understanding. - -## Quick Reference - -| Tool | Use When | Example | -|------|----------|---------| -| \`search\` | Finding code by meaning | "user authentication flow" | -| \`cypher\` | Traversing relationships | Finding what calls a function | -| \`blastRadius\` | Before refactoring | What depends on this class? | -| \`context\` | Understanding project | Get overview, hotspots | -| \`grep\` | Exact pattern match | Find all TODO comments | -| \`read\` | Reading file content | View actual code | -| \`highlight\` | Visual feedback | Show user what you found | - -## Node Types -File, Folder, Function, Class, Interface, Method, CodeElement - -## Relation Types -CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS - -## Cypher Query Examples - -\`\`\`cypher --- Find functions called by a specific function -MATCH (f:Function {name: 'handleRequest'})-[:CodeRelation {type:'CALLS'}]->(called) -RETURN called.name, called.filePath - --- Find class hierarchy -MATCH (c:Class)-[:CodeRelation {type:'EXTENDS'}*1..3]->(parent) -RETURN c.name, collect(parent.name) as ancestors -\`\`\` - -## Workflow - -1. Run \`context\` to understand project structure -2. Use \`search\` to find relevant code -3. Use \`blastRadius\` before making changes -4. Use \`read\` to examine actual code -`; - -export async function generateGuidanceCommand(options: GenerateOptions): Promise { - const outputDir = options.output || '.'; - - console.log(chalk.cyan('\nšŸ”§ Generating AI agent guidance files...\n')); - - try { - // Create .cursor/rules directory - const cursorRulesDir = join(outputDir, '.cursor', 'rules'); - if (!existsSync(cursorRulesDir)) { - await mkdir(cursorRulesDir, { recursive: true }); - } - - // Write .cursor/rules/gitnexus.mdc - const cursorRulePath = join(cursorRulesDir, 'gitnexus.mdc'); - await writeFile(cursorRulePath, CURSOR_RULES_CONTENT, 'utf-8'); - console.log(chalk.green('āœ“'), 'Created', chalk.bold('.cursor/rules/gitnexus.mdc')); - - // Write AGENTS.md - const agentsPath = join(outputDir, 'AGENTS.md'); - await writeFile(agentsPath, AGENTS_MD_CONTENT, 'utf-8'); - console.log(chalk.green('āœ“'), 'Created', chalk.bold('AGENTS.md')); - - console.log(chalk.green('\nāœ… Guidance files generated successfully!')); - console.log(chalk.dim('\nThese files help AI assistants understand how to use GitNexus MCP tools.')); - console.log(chalk.dim('Commit them to your repo so all team members benefit.\n')); - - } catch (error) { - console.error(chalk.red('\nāŒ Error generating guidance files:'), error); - process.exit(1); - } -} - diff --git a/gitnexus-mcp/src/commands/serve.ts b/gitnexus-mcp/src/commands/serve.ts index ed6039ef0..524e40d60 100644 --- a/gitnexus-mcp/src/commands/serve.ts +++ b/gitnexus-mcp/src/commands/serve.ts @@ -3,11 +3,11 @@ * * Starts the MCP server that bridges external AI agents to GitNexus. * - Listens on stdio for MCP protocol (from AI tools) - * - Connects to daemon via WebSocket (client mode) + * - Hosts a local WebSocket bridge for the GitNexus browser app */ import { startMCPServer } from '../mcp/server.js'; -import { DaemonClient } from '../bridge/daemon-client.js'; +import { WebSocketBridge } from '../bridge/websocket-server.js'; interface ServeOptions { port: string; @@ -16,15 +16,13 @@ interface ServeOptions { export async function serveCommand(options: ServeOptions) { const port = parseInt(options.port, 10); - // Connect to daemon as a WebSocket client - const client = new DaemonClient(port); - - try { - await client.connect(); - } catch (error) { - // Daemon not running - provide helpful error - console.error('Failed to connect to GitNexus daemon.'); - console.error('Make sure the daemon is running: npx gitnexus-mcp daemon'); + // Start local WebSocket bridge (browser connects to ws://localhost:) + const client = new WebSocketBridge(port); + const started = await client.start(); + + if (!started) { + console.error(`Failed to start GitNexus browser bridge on port ${port}.`); + console.error('Another process is already using this port.'); process.exit(1); } diff --git a/gitnexus-mcp/src/commands/setup.ts b/gitnexus-mcp/src/commands/setup.ts deleted file mode 100644 index 90470d2fd..000000000 --- a/gitnexus-mcp/src/commands/setup.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Setup Command - * - * Detects installed AI tools (Cursor, Claude Code, Windsurf) and - * configures their MCP settings to use GitNexus. - */ - -import chalk from 'chalk'; -import ora from 'ora'; -import { detectIDEs, type IDE } from '../config/detect-ides.js'; -import { injectMCPConfig } from '../config/inject-config.js'; -import { getBridgePath } from '../config/paths.js'; - -export async function setupCommand() { - console.log(chalk.blue('\nšŸ”§ GitNexus MCP Setup\n')); - - // Step 1: Get bridge path (this CLI itself is the bridge) - const spinner = ora('Locating bridge...').start(); - const bridgePath = getBridgePath(); - spinner.succeed(`Bridge located at ${chalk.dim(bridgePath)}`); - - // Step 2: Detect AI tools - spinner.start('Detecting AI tools...'); - const ides = await detectIDEs(); - - if (ides.length === 0) { - spinner.warn('No supported AI tools detected'); - console.log(chalk.yellow('\nSupported tools: Cursor, Claude Code, Windsurf, VS Code')); - console.log('Install one of these tools to use GitNexus MCP.\n'); - return; - } - - spinner.succeed(`Found: ${ides.map(i => chalk.cyan(i.name)).join(', ')}`); - - // Step 3: Inject MCP config into each IDE - for (const ide of ides) { - spinner.start(`Configuring ${ide.name}...`); - try { - await injectMCPConfig(ide, bridgePath); - spinner.succeed(`Configured ${chalk.cyan(ide.name)}`); - } catch (error) { - spinner.fail(`Failed to configure ${ide.name}: ${error}`); - } - } - - // Success message - console.log(chalk.green('\nāœ… Setup complete!\n')); - console.log('Next steps:'); - console.log(' 1. Open GitNexus in your browser'); - console.log(' 2. Load a codebase'); - console.log(' 3. Click the MCP toggle to connect'); - console.log(' 4. Your AI tools can now use GitNexus!\n'); - - console.log(chalk.dim('Available tools: search, cypher, blastRadius, highlight\n')); -} diff --git a/gitnexus-mcp/src/config/detect-ides.ts b/gitnexus-mcp/src/config/detect-ides.ts deleted file mode 100644 index 807645de3..000000000 --- a/gitnexus-mcp/src/config/detect-ides.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * IDE Detection - * - * Detects installed AI coding tools by checking for their config directories. - * Supports Windows, macOS, and Linux paths. - */ - -import { existsSync } from 'fs'; -import { homedir, platform } from 'os'; -import { join } from 'path'; - -export interface IDE { - name: string; - configPath: string; -} - -interface IDECheck { - name: string; - // Paths relative to home directory (first found is used) - paths: { - win32: string[]; - darwin: string[]; - linux: string[]; - }; -} - -const IDE_CHECKS: IDECheck[] = [ - { - name: 'Cursor', - paths: { - win32: ['AppData/Roaming/Cursor/User/globalStorage/mcp.json', '.cursor/mcp.json'], - darwin: ['.cursor/mcp.json', 'Library/Application Support/Cursor/User/globalStorage/mcp.json'], - linux: ['.cursor/mcp.json', '.config/Cursor/User/globalStorage/mcp.json'], - }, - }, - { - name: 'Claude Code', - paths: { - win32: ['.claude/mcp.json', 'AppData/Roaming/Claude/mcp.json'], - darwin: ['.claude/mcp.json', 'Library/Application Support/Claude/mcp.json'], - linux: ['.claude/mcp.json', '.config/Claude/mcp.json'], - }, - }, - { - name: 'Windsurf', - paths: { - win32: ['.windsurf/mcp.json', '.codeium/windsurf/mcp.json'], - darwin: ['.windsurf/mcp.json', '.codeium/windsurf/mcp.json'], - linux: ['.windsurf/mcp.json', '.codeium/windsurf/mcp.json'], - }, - }, - { - name: 'VS Code', - paths: { - win32: ['AppData/Roaming/Code/User/globalStorage/mcp.json'], - darwin: ['Library/Application Support/Code/User/globalStorage/mcp.json'], - linux: ['.config/Code/User/globalStorage/mcp.json'], - }, - }, - { - name: 'Antigravity', - paths: { - win32: ['.gemini/antigravity/mcp_config.json'], - darwin: ['.gemini/antigravity/mcp_config.json'], - linux: ['.gemini/antigravity/mcp_config.json'], - }, - }, - { - name: 'OpenCode', - paths: { - win32: ['.config/opencode/opencode.json', 'opencode.json'], - darwin: ['.config/opencode/opencode.json', 'opencode.json'], - linux: ['.config/opencode/opencode.json', 'opencode.json'], - }, - }, -]; - -export async function detectIDEs(): Promise { - const home = homedir(); - const os = platform() as 'win32' | 'darwin' | 'linux'; - const ides: IDE[] = []; - - for (const check of IDE_CHECKS) { - const paths = check.paths[os] || check.paths.linux; - - for (const relPath of paths) { - const configPath = join(home, relPath); - // Check if the parent directory exists (IDE is installed) - const dirPath = configPath.replace(/[/\\]mcp\.json$/, ''); - const parentDir = dirPath.split(/[/\\]/).slice(0, -1).join('/'); - - // Look for IDE installation markers - const ideDirExists = existsSync(dirPath) || existsSync(parentDir); - - if (ideDirExists) { - ides.push({ name: check.name, configPath }); - break; // Found this IDE, move to next - } - } - } - - return ides; -} diff --git a/gitnexus-mcp/src/config/inject-config.ts b/gitnexus-mcp/src/config/inject-config.ts deleted file mode 100644 index 8a7ba5de4..000000000 --- a/gitnexus-mcp/src/config/inject-config.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * MCP Config Injection - * - * Adds GitNexus MCP server to an IDE's configuration. - * Handles different config formats for different tools. - */ - -import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; -import { dirname } from 'path'; -import { type IDE } from './detect-ides.js'; - -interface MCPServerEntry { - command: string; - args?: string[]; - env?: Record; -} - -interface StandardMCPConfig { - mcpServers?: Record; -} - -// OpenCode uses a different format with 'mcp' key -interface OpenCodeConfig { - mcp?: Record; - [key: string]: any; -} - -export async function injectMCPConfig(ide: IDE, bridgePath: string): Promise { - // Create directory structure if needed - if (!existsSync(ide.configPath)) { - const dir = dirname(ide.configPath); - mkdirSync(dir, { recursive: true }); - } - - // Handle OpenCode's different format - if (ide.name === 'OpenCode') { - await injectOpenCodeConfig(ide.configPath, bridgePath); - return; - } - - // Standard MCP format (Cursor, Claude Code, Windsurf, VS Code, Antigravity) - let config: StandardMCPConfig = { mcpServers: {} }; - - // Read existing config if present - if (existsSync(ide.configPath)) { - try { - const content = readFileSync(ide.configPath, 'utf-8'); - config = JSON.parse(content); - } catch { - // Ignore parse errors, use empty config - } - } - - // Ensure mcpServers object exists - config.mcpServers = config.mcpServers || {}; - - // Add GitNexus MCP server configuration - config.mcpServers.gitnexus = { - command: bridgePath, - args: ['serve'], - }; - - // Write updated config - writeFileSync(ide.configPath, JSON.stringify(config, null, 2)); -} - -async function injectOpenCodeConfig(configPath: string, bridgePath: string): Promise { - let config: OpenCodeConfig = {}; - - // Read existing config if present - if (existsSync(configPath)) { - try { - const content = readFileSync(configPath, 'utf-8'); - config = JSON.parse(content); - } catch { - // Ignore parse errors - } - } - - // Ensure mcp object exists - config.mcp = config.mcp || {}; - - // Add GitNexus MCP server (OpenCode format) - config.mcp.gitnexus = { - type: 'local', - command: bridgePath, - args: ['serve'], - }; - - // Write updated config - writeFileSync(configPath, JSON.stringify(config, null, 2)); -} - diff --git a/gitnexus-mcp/src/config/paths.ts b/gitnexus-mcp/src/config/paths.ts deleted file mode 100644 index bfc989082..000000000 --- a/gitnexus-mcp/src/config/paths.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Path Utilities - * - * Resolves paths for the MCP bridge executable. - */ - -import { fileURLToPath } from 'url'; -import { dirname, resolve } from 'path'; - -/** - * Get the path to this CLI executable. - * When installed via npm, this will be in node_modules/.bin/ - * The MCP config needs to point to the actual executable. - */ -export function getBridgePath(): string { - // When running as ES module, we need to resolve from import.meta.url - // But for the installed package, we want the bin path - - // Get the path to this module - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - - // Go up from dist/config/ to the package root, then to the bin - const packageRoot = resolve(__dirname, '..', '..'); - const binPath = resolve(packageRoot, 'dist', 'cli.js'); - - // Return as node command since it's a JS file - return `node ${binPath}`; -} - -/** - * Get user's GitNexus config directory - */ -export function getGitNexusConfigDir(): string { - const home = process.env.HOME || process.env.USERPROFILE || ''; - return resolve(home, '.gitnexus'); -} diff --git a/gitnexus-mcp/src/mcp/server.ts b/gitnexus-mcp/src/mcp/server.ts index 2a32a9391..81b663613 100644 --- a/gitnexus-mcp/src/mcp/server.ts +++ b/gitnexus-mcp/src/mcp/server.ts @@ -19,7 +19,7 @@ import { ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { GITNEXUS_TOOLS } from './tools.js'; -import type { DaemonClient, CodebaseContext } from '../bridge/daemon-client.js'; +import type { CodebaseContext } from '../bridge/websocket-server.js'; // Interface for anything that can call tools (DaemonClient or WebSocketBridge) interface ToolCaller { diff --git a/gitnexus-mcp/src/mcp/tools.ts b/gitnexus-mcp/src/mcp/tools.ts index a822b1abb..4cc60a71d 100644 --- a/gitnexus-mcp/src/mcp/tools.ts +++ b/gitnexus-mcp/src/mcp/tools.ts @@ -135,24 +135,29 @@ RETURNS: {filePath, content, language, lines}`, { name: 'blastRadius', description: `Analyze the impact of changing a code element. -Returns all nodes that would be affected by modifying the target. +Returns all nodes affected by modifying the target, with distance, edge type, and confidence. USE BEFORE making changes to understand ripple effects. -Shows direct and indirect dependencies up to N hops. -RETURNS: { - target: {...}, - affected: [{name, type, filePath, distance}...], - totalAffected: number, - riskLevel: "low" | "medium" | "high" -}`, +Output format (compact tabular): + Type|Name|File:Line|EdgeType|Confidence% + +EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS +Confidence: 100% = certain, <80% = fuzzy match [fuzzy] + +Depth groups: +- d=1: WILL BREAK (direct callers/importers) +- d=2: LIKELY AFFECTED (indirect) +- d=3: MAY NEED TESTING (transitive)`, inputSchema: { type: 'object', properties: { - nodeId: { type: 'string', description: 'Node ID or name to analyze' }, - hops: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 }, + target: { type: 'string', description: 'Name of function, class, or file to analyze' }, + direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' }, + maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 }, + relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, CONTAINS, DEFINES (default: usage-based)' }, }, - required: ['nodeId'], + required: ['target', 'direction'], }, }, {