fixed blast radius analyses, method calls relations failing solved, mcp simplified ( need to publish into npm )

This commit is contained in:
abhigyanpatwari 2026-01-18 06:05:44 +05:30
parent 055f13f9c8
commit 87fedd17e7
13 changed files with 148 additions and 931 deletions

View file

@ -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",

View file

@ -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<string, RequestResolver> = 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<void> {
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<any> {
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;
}
}

View file

@ -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<string, RequestResolver> = 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();
}
}

View file

@ -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 <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 <port>', 'WebSocket port', '54319')
.action(daemonCommand);
program
.command('serve')
.description('Start MCP server for an AI tool (connects to daemon)')
.option('-p, --port <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 <dir>', '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 <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 <port>]');
process.exit(1);
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});

View file

@ -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<WebSocket> = new Set();
const pendingRequests: Map<string, WebSocket> = 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<WebSocket, AgentSession> = new Map();
const agentInstanceCounters: Map<string, number> = 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);
});
}

View file

@ -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<void> {
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);
}
}

View file

@ -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:<port>)
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);
}

View file

@ -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'));
}

View file

@ -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<IDE[]> {
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;
}

View file

@ -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<string, string>;
}
interface StandardMCPConfig {
mcpServers?: Record<string, MCPServerEntry>;
}
// OpenCode uses a different format with 'mcp' key
interface OpenCodeConfig {
mcp?: Record<string, {
type?: string;
command: string;
args?: string[];
}>;
[key: string]: any;
}
export async function injectMCPConfig(ide: IDE, bridgePath: string): Promise<void> {
// 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<void> {
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));
}

View file

@ -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');
}

View file

@ -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 {

View file

@ -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'],
},
},
{