diff --git a/packages/mcp-server/CURSOR_SETUP.md b/packages/mcp-server/CURSOR_SETUP.md new file mode 100644 index 0000000..37c6a84 --- /dev/null +++ b/packages/mcp-server/CURSOR_SETUP.md @@ -0,0 +1,145 @@ +# Cursor MCP Setup Guide + +This guide shows how to integrate the dev-agent MCP server with Cursor IDE. + +## Prerequisites + +1. Build the MCP server: + ```bash + cd /path/to/dev-agent + pnpm install + pnpm build + ``` + +2. Ensure the repository is indexed: + ```bash + pnpm dev scan + ``` + +## Configuration + +### 1. Locate Cursor's MCP Config + +The MCP configuration file is located at: +- **macOS**: `~/Library/Application Support/Cursor/User/globalStorage/mcp.json` +- **Linux**: `~/.config/Cursor/User/globalStorage/mcp.json` +- **Windows**: `%APPDATA%\Cursor\User\globalStorage\mcp.json` + +### 2. Add dev-agent MCP Server + +Create or update the `mcp.json` file with the following configuration: + +```json +{ + "mcpServers": { + "dev-agent": { + "command": "node", + "args": [ + "/absolute/path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js" + ], + "env": { + "REPOSITORY_PATH": "/absolute/path/to/your/repository", + "LOG_LEVEL": "info" + } + } + } +} +``` + +**Important:** Replace the paths with your actual absolute paths. + +### 3. Restart Cursor + +After updating the configuration, restart Cursor to load the MCP server. + +## Verification + +1. Open Cursor +2. Try using the `dev_search` tool in a chat: + ``` + Search for "authentication middleware" in the codebase + ``` + +3. The MCP server should respond with semantic search results + +## Available Tools + +### `dev_search` + +Semantic code search across your repository. + +**Parameters:** +- `query` (required): Natural language search query +- `format` (optional): `compact` (default) or `verbose` +- `limit` (optional): Number of results (1-50, default: 10) +- `scoreThreshold` (optional): Minimum relevance score (0-1, default: 0) + +**Example:** +``` +dev_search: + query: "user authentication logic" + format: compact + limit: 5 +``` + +## Troubleshooting + +### Server Not Starting + +1. Check Cursor's logs (Help > Show Logs) +2. Verify the paths in `mcp.json` are absolute and correct +3. Ensure the server builds successfully: `pnpm build` +4. Test the server manually: + ```bash + echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \ + node /path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js + ``` + +### Repository Not Indexed + +If searches return no results: +```bash +cd /path/to/your/repository +/path/to/dev-agent/packages/cli/dist/index.js scan +``` + +### Logs + +Set `LOG_LEVEL` to `debug` in the env section of `mcp.json` for more verbose logging: +```json +"env": { + "REPOSITORY_PATH": "/path/to/repo", + "LOG_LEVEL": "debug" +} +``` + +## Multiple Repositories + +To use dev-agent with multiple repositories, add multiple server configurations: + +```json +{ + "mcpServers": { + "dev-agent-project-a": { + "command": "node", + "args": ["/path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js"], + "env": { + "REPOSITORY_PATH": "/path/to/project-a" + } + }, + "dev-agent-project-b": { + "command": "node", + "args": ["/path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js"], + "env": { + "REPOSITORY_PATH": "/path/to/project-b" + } + } + } +} +``` + +## Next Steps + +- See [README.md](./README.md) for general MCP server documentation +- See [../../docs/WORKFLOW.md](../../docs/WORKFLOW.md) for the dev-agent workflow + diff --git a/packages/mcp-server/bin/dev-agent-mcp.ts b/packages/mcp-server/bin/dev-agent-mcp.ts new file mode 100644 index 0000000..5fcb169 --- /dev/null +++ b/packages/mcp-server/bin/dev-agent-mcp.ts @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/** + * dev-agent MCP Server Entry Point + * Starts the MCP server with stdio transport for AI tools (Claude, Cursor, etc.) + */ + +import { RepositoryIndexer } from '@lytics/dev-agent-core'; +import { SearchAdapter } from '../src/adapters/built-in/search-adapter'; +import { MCPServer } from '../src/server/mcp-server'; + +// Get config from environment +const repositoryPath = process.env.REPOSITORY_PATH || process.cwd(); +const vectorStorePath = + process.env.VECTOR_STORE_PATH || `${repositoryPath}/.dev-agent/vectors.lance`; +const logLevel = (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') || 'info'; + +async function main() { + try { + // Initialize repository indexer + const indexer = new RepositoryIndexer({ + repositoryPath, + vectorStorePath, + }); + + await indexer.initialize(); + + // Create and register adapters + const searchAdapter = new SearchAdapter({ + repositoryIndexer: indexer, + defaultFormat: 'compact', + defaultLimit: 10, + }); + + // Create MCP server + const server = new MCPServer({ + serverInfo: { + name: 'dev-agent', + version: '0.1.0', + }, + config: { + repositoryPath, + logLevel, + }, + transport: 'stdio', + adapters: [searchAdapter], + }); + + // Handle graceful shutdown + const shutdown = async () => { + await server.stop(); + await indexer.close(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + // Start server + await server.start(); + + // Keep process alive (server runs until stdin closes or signal received) + } catch (error) { + console.error('Failed to start MCP server:', error); + process.exit(1); + } +} + +main(); diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index b35d48a..fccd0da 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -6,7 +6,7 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "bin": { - "dev-agent-mcp": "./dist/index.js" + "dev-agent-mcp": "./dist/bin/dev-agent-mcp.js" }, "scripts": { "build": "tsc", diff --git a/packages/mcp-server/src/server/mcp-server.ts b/packages/mcp-server/src/server/mcp-server.ts index e4e0ebf..4931cbe 100644 --- a/packages/mcp-server/src/server/mcp-server.ts +++ b/packages/mcp-server/src/server/mcp-server.ts @@ -12,11 +12,9 @@ import type { ErrorCode, InitializeResult, JSONRPCRequest, - JSONRPCResponse, MCPMethod, ServerCapabilities, ServerInfo, - ToolCall, } from './protocol/types'; import { StdioTransport } from './transport/stdio-transport'; import type { Transport, TransportMessage } from './transport/transport'; @@ -32,9 +30,10 @@ export interface MCPServerConfig { export class MCPServer { private registry: AdapterRegistry; private transport: Transport; - private logger = new ConsoleLogger('[MCP Server]'); + private logger = new ConsoleLogger('[MCP Server]', 'debug'); // Enable debug logging private config: Config; private serverInfo: ServerInfo; + private clientProtocolVersion?: string; constructor(config: MCPServerConfig) { this.config = config.config; @@ -105,17 +104,39 @@ export class MCPServer { * Handle incoming MCP message */ private async handleMessage(message: TransportMessage): Promise { - // Only handle requests (not notifications for now) + this.logger.debug('Raw message received', { + type: typeof message, + isRequest: JSONRPCHandler.isRequest(message), + preview: JSON.stringify(message).substring(0, 200), + }); + + // Handle notifications if (!JSONRPCHandler.isRequest(message)) { - this.logger.debug('Ignoring notification', { method: message.method }); + const method = (message as { method: string }).method; + this.logger.info('Received notification', { method }); + + // The 'initialized' notification is sent by the client after receiving + // the initialize response. We acknowledge it but don't need to respond. + if (method === 'initialized') { + this.logger.info('Client initialized successfully'); + } + return; } const request = message as JSONRPCRequest; + this.logger.info('Received request', { method: request.method, id: request.id }); try { const result = await this.routeRequest(request); - const response = JSONRPCHandler.createResponse(request.id!, result); + // request.id is guaranteed to be defined for requests (checked by isRequest) + const requestId = request.id ?? 0; + const response = JSONRPCHandler.createResponse(requestId, result); + this.logger.debug('Sending response', { + id: request.id, + method: request.method, + responsePreview: JSON.stringify(response).substring(0, 200), + }); await this.transport.send(response); } catch (error) { this.logger.error('Request handling failed', { @@ -124,7 +145,8 @@ export class MCPServer { }); const jsonrpcError = error as { code: ErrorCode; message: string; data?: unknown }; - const errorResponse = JSONRPCHandler.createErrorResponse(request.id, jsonrpcError); + const requestId = request.id ?? 0; + const errorResponse = JSONRPCHandler.createErrorResponse(requestId, jsonrpcError); await this.transport.send(errorResponse); } } @@ -137,7 +159,7 @@ export class MCPServer { switch (method) { case 'initialize': - return this.handleInitialize(); + return this.handleInitialize(request.params as { protocolVersion?: string }); case 'tools/list': return this.handleToolsList(); @@ -161,7 +183,16 @@ export class MCPServer { /** * Handle initialize request */ - private handleInitialize(): InitializeResult { + private handleInitialize(params?: { protocolVersion?: string }): InitializeResult { + // Store the client's protocol version and echo it back + // MCP uses date-based versioning (e.g., "2025-06-18") + this.clientProtocolVersion = params?.protocolVersion || '1.0'; + + this.logger.debug('Initialize request', { + clientProtocolVersion: this.clientProtocolVersion, + clientParams: params, + }); + const capabilities: ServerCapabilities = { tools: { supported: true }, resources: { supported: false }, // Not yet implemented @@ -169,7 +200,7 @@ export class MCPServer { }; return { - protocolVersion: '1.0', + protocolVersion: this.clientProtocolVersion, capabilities, serverInfo: this.serverInfo, }; diff --git a/packages/mcp-server/src/server/transport/stdio-transport.ts b/packages/mcp-server/src/server/transport/stdio-transport.ts index 56cd501..229da5a 100644 --- a/packages/mcp-server/src/server/transport/stdio-transport.ts +++ b/packages/mcp-server/src/server/transport/stdio-transport.ts @@ -20,9 +20,9 @@ export class StdioTransport extends Transport { } // Create readline interface for line-by-line input + // Note: We don't specify 'output' to avoid readline echoing input to stdout this.readline = readline.createInterface({ input: process.stdin, - output: process.stdout, terminal: false, }); diff --git a/packages/mcp-server/src/utils/logger.ts b/packages/mcp-server/src/utils/logger.ts index 72dc099..082fb06 100644 --- a/packages/mcp-server/src/utils/logger.ts +++ b/packages/mcp-server/src/utils/logger.ts @@ -5,20 +5,30 @@ import type { Logger } from '../adapters/types'; export class ConsoleLogger implements Logger { - constructor(private prefix = '[MCP]') {} + constructor( + private prefix = '[MCP]', + private minLevel: 'debug' | 'info' | 'warn' | 'error' = 'info' + ) {} debug(message: string, meta?: Record): void { - if (process.env.DEBUG) { - console.debug(`${this.prefix} DEBUG:`, message, meta || ''); + if (this.minLevel === 'debug') { + // MCP requires all logs on stderr (stdout is for JSON-RPC only) + console.error(`${this.prefix} DEBUG:`, message, meta || ''); } } info(message: string, meta?: Record): void { - console.info(`${this.prefix} INFO:`, message, meta ? JSON.stringify(meta) : ''); + if (this.minLevel === 'debug' || this.minLevel === 'info') { + // MCP requires all logs on stderr (stdout is for JSON-RPC only) + console.error(`${this.prefix} INFO:`, message, meta ? JSON.stringify(meta) : ''); + } } warn(message: string, meta?: Record): void { - console.warn(`${this.prefix} WARN:`, message, meta ? JSON.stringify(meta) : ''); + if (this.minLevel !== 'error') { + // MCP requires all logs on stderr (stdout is for JSON-RPC only) + console.error(`${this.prefix} WARN:`, message, meta ? JSON.stringify(meta) : ''); + } } error(message: string | Error, meta?: Record): void { diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index 4da598a..c97d7e9 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -2,12 +2,12 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src", + "rootDir": ".", "composite": true, "declaration": true, "declarationMap": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "bin/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"], "references": [ { "path": "../core" },