Skip to content

Commit ffbc759

Browse files
committed
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.
1 parent 9533634 commit ffbc759

File tree

7 files changed

+273
-19
lines changed

7 files changed

+273
-19
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Cursor MCP Setup Guide
2+
3+
This guide shows how to integrate the dev-agent MCP server with Cursor IDE.
4+
5+
## Prerequisites
6+
7+
1. Build the MCP server:
8+
```bash
9+
cd /path/to/dev-agent
10+
pnpm install
11+
pnpm build
12+
```
13+
14+
2. Ensure the repository is indexed:
15+
```bash
16+
pnpm dev scan
17+
```
18+
19+
## Configuration
20+
21+
### 1. Locate Cursor's MCP Config
22+
23+
The MCP configuration file is located at:
24+
- **macOS**: `~/Library/Application Support/Cursor/User/globalStorage/mcp.json`
25+
- **Linux**: `~/.config/Cursor/User/globalStorage/mcp.json`
26+
- **Windows**: `%APPDATA%\Cursor\User\globalStorage\mcp.json`
27+
28+
### 2. Add dev-agent MCP Server
29+
30+
Create or update the `mcp.json` file with the following configuration:
31+
32+
```json
33+
{
34+
"mcpServers": {
35+
"dev-agent": {
36+
"command": "node",
37+
"args": [
38+
"/absolute/path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js"
39+
],
40+
"env": {
41+
"REPOSITORY_PATH": "/absolute/path/to/your/repository",
42+
"LOG_LEVEL": "info"
43+
}
44+
}
45+
}
46+
}
47+
```
48+
49+
**Important:** Replace the paths with your actual absolute paths.
50+
51+
### 3. Restart Cursor
52+
53+
After updating the configuration, restart Cursor to load the MCP server.
54+
55+
## Verification
56+
57+
1. Open Cursor
58+
2. Try using the `dev_search` tool in a chat:
59+
```
60+
Search for "authentication middleware" in the codebase
61+
```
62+
63+
3. The MCP server should respond with semantic search results
64+
65+
## Available Tools
66+
67+
### `dev_search`
68+
69+
Semantic code search across your repository.
70+
71+
**Parameters:**
72+
- `query` (required): Natural language search query
73+
- `format` (optional): `compact` (default) or `verbose`
74+
- `limit` (optional): Number of results (1-50, default: 10)
75+
- `scoreThreshold` (optional): Minimum relevance score (0-1, default: 0)
76+
77+
**Example:**
78+
```
79+
dev_search:
80+
query: "user authentication logic"
81+
format: compact
82+
limit: 5
83+
```
84+
85+
## Troubleshooting
86+
87+
### Server Not Starting
88+
89+
1. Check Cursor's logs (Help > Show Logs)
90+
2. Verify the paths in `mcp.json` are absolute and correct
91+
3. Ensure the server builds successfully: `pnpm build`
92+
4. Test the server manually:
93+
```bash
94+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | \
95+
node /path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js
96+
```
97+
98+
### Repository Not Indexed
99+
100+
If searches return no results:
101+
```bash
102+
cd /path/to/your/repository
103+
/path/to/dev-agent/packages/cli/dist/index.js scan
104+
```
105+
106+
### Logs
107+
108+
Set `LOG_LEVEL` to `debug` in the env section of `mcp.json` for more verbose logging:
109+
```json
110+
"env": {
111+
"REPOSITORY_PATH": "/path/to/repo",
112+
"LOG_LEVEL": "debug"
113+
}
114+
```
115+
116+
## Multiple Repositories
117+
118+
To use dev-agent with multiple repositories, add multiple server configurations:
119+
120+
```json
121+
{
122+
"mcpServers": {
123+
"dev-agent-project-a": {
124+
"command": "node",
125+
"args": ["/path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js"],
126+
"env": {
127+
"REPOSITORY_PATH": "/path/to/project-a"
128+
}
129+
},
130+
"dev-agent-project-b": {
131+
"command": "node",
132+
"args": ["/path/to/dev-agent/packages/mcp-server/dist/bin/dev-agent-mcp.js"],
133+
"env": {
134+
"REPOSITORY_PATH": "/path/to/project-b"
135+
}
136+
}
137+
}
138+
}
139+
```
140+
141+
## Next Steps
142+
143+
- See [README.md](./README.md) for general MCP server documentation
144+
- See [../../docs/WORKFLOW.md](../../docs/WORKFLOW.md) for the dev-agent workflow
145+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env node
2+
/**
3+
* dev-agent MCP Server Entry Point
4+
* Starts the MCP server with stdio transport for AI tools (Claude, Cursor, etc.)
5+
*/
6+
7+
import { RepositoryIndexer } from '@lytics/dev-agent-core';
8+
import { SearchAdapter } from '../src/adapters/built-in/search-adapter';
9+
import { MCPServer } from '../src/server/mcp-server';
10+
11+
// Get config from environment
12+
const repositoryPath = process.env.REPOSITORY_PATH || process.cwd();
13+
const vectorStorePath =
14+
process.env.VECTOR_STORE_PATH || `${repositoryPath}/.dev-agent/vectors.lance`;
15+
const logLevel = (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') || 'info';
16+
17+
async function main() {
18+
try {
19+
// Initialize repository indexer
20+
const indexer = new RepositoryIndexer({
21+
repositoryPath,
22+
vectorStorePath,
23+
});
24+
25+
await indexer.initialize();
26+
27+
// Create and register adapters
28+
const searchAdapter = new SearchAdapter({
29+
repositoryIndexer: indexer,
30+
defaultFormat: 'compact',
31+
defaultLimit: 10,
32+
});
33+
34+
// Create MCP server
35+
const server = new MCPServer({
36+
serverInfo: {
37+
name: 'dev-agent',
38+
version: '0.1.0',
39+
},
40+
config: {
41+
repositoryPath,
42+
logLevel,
43+
},
44+
transport: 'stdio',
45+
adapters: [searchAdapter],
46+
});
47+
48+
// Handle graceful shutdown
49+
const shutdown = async () => {
50+
await server.stop();
51+
await indexer.close();
52+
process.exit(0);
53+
};
54+
55+
process.on('SIGINT', shutdown);
56+
process.on('SIGTERM', shutdown);
57+
58+
// Start server
59+
await server.start();
60+
61+
// Keep process alive (server runs until stdin closes or signal received)
62+
} catch (error) {
63+
console.error('Failed to start MCP server:', error);
64+
process.exit(1);
65+
}
66+
}
67+
68+
main();

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",
88
"bin": {
9-
"dev-agent-mcp": "./dist/index.js"
9+
"dev-agent-mcp": "./dist/bin/dev-agent-mcp.js"
1010
},
1111
"scripts": {
1212
"build": "tsc",

packages/mcp-server/src/server/mcp-server.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ import type {
1212
ErrorCode,
1313
InitializeResult,
1414
JSONRPCRequest,
15-
JSONRPCResponse,
1615
MCPMethod,
1716
ServerCapabilities,
1817
ServerInfo,
19-
ToolCall,
2018
} from './protocol/types';
2119
import { StdioTransport } from './transport/stdio-transport';
2220
import type { Transport, TransportMessage } from './transport/transport';
@@ -32,9 +30,10 @@ export interface MCPServerConfig {
3230
export class MCPServer {
3331
private registry: AdapterRegistry;
3432
private transport: Transport;
35-
private logger = new ConsoleLogger('[MCP Server]');
33+
private logger = new ConsoleLogger('[MCP Server]', 'debug'); // Enable debug logging
3634
private config: Config;
3735
private serverInfo: ServerInfo;
36+
private clientProtocolVersion?: string;
3837

3938
constructor(config: MCPServerConfig) {
4039
this.config = config.config;
@@ -105,17 +104,39 @@ export class MCPServer {
105104
* Handle incoming MCP message
106105
*/
107106
private async handleMessage(message: TransportMessage): Promise<void> {
108-
// Only handle requests (not notifications for now)
107+
this.logger.debug('Raw message received', {
108+
type: typeof message,
109+
isRequest: JSONRPCHandler.isRequest(message),
110+
preview: JSON.stringify(message).substring(0, 200),
111+
});
112+
113+
// Handle notifications
109114
if (!JSONRPCHandler.isRequest(message)) {
110-
this.logger.debug('Ignoring notification', { method: message.method });
115+
const method = (message as { method: string }).method;
116+
this.logger.info('Received notification', { method });
117+
118+
// The 'initialized' notification is sent by the client after receiving
119+
// the initialize response. We acknowledge it but don't need to respond.
120+
if (method === 'initialized') {
121+
this.logger.info('Client initialized successfully');
122+
}
123+
111124
return;
112125
}
113126

114127
const request = message as JSONRPCRequest;
128+
this.logger.info('Received request', { method: request.method, id: request.id });
115129

116130
try {
117131
const result = await this.routeRequest(request);
118-
const response = JSONRPCHandler.createResponse(request.id!, result);
132+
// request.id is guaranteed to be defined for requests (checked by isRequest)
133+
const requestId = request.id ?? 0;
134+
const response = JSONRPCHandler.createResponse(requestId, result);
135+
this.logger.debug('Sending response', {
136+
id: request.id,
137+
method: request.method,
138+
responsePreview: JSON.stringify(response).substring(0, 200),
139+
});
119140
await this.transport.send(response);
120141
} catch (error) {
121142
this.logger.error('Request handling failed', {
@@ -124,7 +145,8 @@ export class MCPServer {
124145
});
125146

126147
const jsonrpcError = error as { code: ErrorCode; message: string; data?: unknown };
127-
const errorResponse = JSONRPCHandler.createErrorResponse(request.id, jsonrpcError);
148+
const requestId = request.id ?? 0;
149+
const errorResponse = JSONRPCHandler.createErrorResponse(requestId, jsonrpcError);
128150
await this.transport.send(errorResponse);
129151
}
130152
}
@@ -137,7 +159,7 @@ export class MCPServer {
137159

138160
switch (method) {
139161
case 'initialize':
140-
return this.handleInitialize();
162+
return this.handleInitialize(request.params as { protocolVersion?: string });
141163

142164
case 'tools/list':
143165
return this.handleToolsList();
@@ -161,15 +183,24 @@ export class MCPServer {
161183
/**
162184
* Handle initialize request
163185
*/
164-
private handleInitialize(): InitializeResult {
186+
private handleInitialize(params?: { protocolVersion?: string }): InitializeResult {
187+
// Store the client's protocol version and echo it back
188+
// MCP uses date-based versioning (e.g., "2025-06-18")
189+
this.clientProtocolVersion = params?.protocolVersion || '1.0';
190+
191+
this.logger.debug('Initialize request', {
192+
clientProtocolVersion: this.clientProtocolVersion,
193+
clientParams: params,
194+
});
195+
165196
const capabilities: ServerCapabilities = {
166197
tools: { supported: true },
167198
resources: { supported: false }, // Not yet implemented
168199
prompts: { supported: false }, // Not yet implemented
169200
};
170201

171202
return {
172-
protocolVersion: '1.0',
203+
protocolVersion: this.clientProtocolVersion,
173204
capabilities,
174205
serverInfo: this.serverInfo,
175206
};

packages/mcp-server/src/server/transport/stdio-transport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ export class StdioTransport extends Transport {
2020
}
2121

2222
// Create readline interface for line-by-line input
23+
// Note: We don't specify 'output' to avoid readline echoing input to stdout
2324
this.readline = readline.createInterface({
2425
input: process.stdin,
25-
output: process.stdout,
2626
terminal: false,
2727
});
2828

packages/mcp-server/src/utils/logger.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,30 @@
55
import type { Logger } from '../adapters/types';
66

77
export class ConsoleLogger implements Logger {
8-
constructor(private prefix = '[MCP]') {}
8+
constructor(
9+
private prefix = '[MCP]',
10+
private minLevel: 'debug' | 'info' | 'warn' | 'error' = 'info'
11+
) {}
912

1013
debug(message: string, meta?: Record<string, unknown>): void {
11-
if (process.env.DEBUG) {
12-
console.debug(`${this.prefix} DEBUG:`, message, meta || '');
14+
if (this.minLevel === 'debug') {
15+
// MCP requires all logs on stderr (stdout is for JSON-RPC only)
16+
console.error(`${this.prefix} DEBUG:`, message, meta || '');
1317
}
1418
}
1519

1620
info(message: string, meta?: Record<string, unknown>): void {
17-
console.info(`${this.prefix} INFO:`, message, meta ? JSON.stringify(meta) : '');
21+
if (this.minLevel === 'debug' || this.minLevel === 'info') {
22+
// MCP requires all logs on stderr (stdout is for JSON-RPC only)
23+
console.error(`${this.prefix} INFO:`, message, meta ? JSON.stringify(meta) : '');
24+
}
1825
}
1926

2027
warn(message: string, meta?: Record<string, unknown>): void {
21-
console.warn(`${this.prefix} WARN:`, message, meta ? JSON.stringify(meta) : '');
28+
if (this.minLevel !== 'error') {
29+
// MCP requires all logs on stderr (stdout is for JSON-RPC only)
30+
console.error(`${this.prefix} WARN:`, message, meta ? JSON.stringify(meta) : '');
31+
}
2232
}
2333

2434
error(message: string | Error, meta?: Record<string, unknown>): void {

packages/mcp-server/tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"extends": "../../tsconfig.json",
33
"compilerOptions": {
44
"outDir": "./dist",
5-
"rootDir": "./src",
5+
"rootDir": ".",
66
"composite": true,
77
"declaration": true,
88
"declarationMap": true
99
},
10-
"include": ["src/**/*"],
10+
"include": ["src/**/*", "bin/**/*"],
1111
"exclude": ["node_modules", "dist", "**/*.test.ts"],
1212
"references": [
1313
{ "path": "../core" },

0 commit comments

Comments
 (0)