From 9481df329058880bbc1a6094939b9c2bbad92004 Mon Sep 17 00:00:00 2001 From: prosdev Date: Mon, 24 Nov 2025 07:44:10 -0800 Subject: [PATCH] fix(mcp-server): fix Cursor integration and protocol compatibility - Echo back client's protocol version (MCP uses date-based versioning like '2025-06-18') - Add handling for 'initialized' notification to prevent premature connection closure - Enable debug logging to aid troubleshooting - Fix logger to write all messages to stderr (not stdout) to avoid JSON-RPC interference - Remove readline output config to prevent echoing to stdout - Add CLI entry point (dev-agent-mcp.ts) for starting the MCP server This fixes the issue where Cursor would immediately close the MCP server connection due to protocol version mismatch and improper stdout usage. The server now successfully connects to Cursor and the dev_search tool is fully functional. --- packages/mcp-server/CURSOR_SETUP.md | 145 ++++++++++++++++++ packages/mcp-server/bin/dev-agent-mcp.ts | 68 ++++++++ packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server/mcp-server.ts | 51 ++++-- .../src/server/transport/stdio-transport.ts | 2 +- packages/mcp-server/src/utils/logger.ts | 20 ++- packages/mcp-server/tsconfig.json | 4 +- 7 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 packages/mcp-server/CURSOR_SETUP.md create mode 100644 packages/mcp-server/bin/dev-agent-mcp.ts 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" },