mcpServer.ts•11.2 kB
import * as http from 'http';
import { Logger } from './logger';
import { LanguageServerTools } from './languageServerTools';
interface MCPRequest {
jsonrpc: '2.0';
id: string | number;
method: string;
params?: any;
}
interface MCPResponse {
jsonrpc: '2.0';
id: string | number;
result?: any;
error?: {
code: number;
message: string;
data?: any;
};
}
export class MCPServer {
private httpServer: http.Server | undefined;
private running = false;
private tools: LanguageServerTools;
constructor(
private port: number,
private logger: Logger
) {
this.tools = new LanguageServerTools(logger);
}
start() {
if (this.running) {
this.logger.warn('MCP server already running');
return;
}
this.httpServer = http.createServer(async (req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// Handle preflight
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Handle GET for health check
if (req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
name: 'vscode-mcp-language-server',
version: '0.1.0',
status: 'running',
transport: 'http'
}));
return;
}
// Handle POST for MCP requests
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const request: MCPRequest = JSON.parse(body);
this.logger.debug('Received request:', request.method);
const response = await this.handleRequest(request);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(response));
} catch (error) {
this.logger.error('Error handling request:', error);
const errorResponse: MCPResponse = {
jsonrpc: '2.0',
id: null as any,
error: {
code: -32700,
message: 'Parse error',
data: String(error)
}
};
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(errorResponse));
}
});
return;
}
// Method not allowed
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
});
this.httpServer.listen(this.port, () => {
this.running = true;
this.logger.info(`MCP HTTP server listening on port ${this.port}`);
});
}
stop() {
if (!this.running) {
return;
}
this.httpServer?.close();
this.running = false;
this.logger.info('MCP server stopped');
}
isRunning(): boolean {
return this.running;
}
private async handleRequest(request: MCPRequest): Promise<MCPResponse> {
try {
const { method, params } = request;
let result: any;
switch (method) {
case 'initialize':
result = this.handleInitialize();
break;
case 'tools/list':
result = this.handleToolsList();
break;
case 'tools/call':
result = await this.handleToolCall(params);
break;
default:
throw new Error(`Unknown method: ${method}`);
}
return {
jsonrpc: '2.0',
id: request.id,
result
};
} catch (error) {
this.logger.error('Error handling request:', error);
return {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32603,
message: 'Internal error',
data: String(error)
}
};
}
}
private handleInitialize() {
return {
protocolVersion: '2024-11-05',
serverInfo: {
name: 'vscode-mcp-language-server',
version: '0.1.0'
},
capabilities: {
tools: {}
}
};
}
private handleToolsList() {
return {
tools: [
{
name: 'vscode_ls_get_symbols',
description: 'Get document symbol outline from VSCode Language Server',
inputSchema: {
type: 'object',
properties: {
uri: {
type: 'string',
description: 'File URI (e.g., file:///path/to/file.js)'
}
},
required: ['uri']
}
},
{
name: 'vscode_ls_find_references',
description: 'Find all references to a symbol at a position',
inputSchema: {
type: 'object',
properties: {
uri: {
type: 'string',
description: 'File URI'
},
line: {
type: 'number',
description: 'Line number (0-indexed)'
},
character: {
type: 'number',
description: 'Character offset (0-indexed)'
}
},
required: ['uri', 'line', 'character']
}
},
{
name: 'vscode_ls_rename',
description: 'Rename a symbol at a position with reference tracking',
inputSchema: {
type: 'object',
properties: {
uri: {
type: 'string',
description: 'File URI'
},
line: {
type: 'number',
description: 'Line number (0-indexed)'
},
character: {
type: 'number',
description: 'Character offset (0-indexed)'
},
newName: {
type: 'string',
description: 'New name for the symbol'
}
},
required: ['uri', 'line', 'character', 'newName']
}
},
{
name: 'vscode_ls_get_definition',
description: 'Get definition location for a symbol',
inputSchema: {
type: 'object',
properties: {
uri: {
type: 'string',
description: 'File URI'
},
line: {
type: 'number',
description: 'Line number (0-indexed)'
},
character: {
type: 'number',
description: 'Character offset (0-indexed)'
}
},
required: ['uri', 'line', 'character']
}
},
{
name: 'vscode_ls_get_hover',
description: 'Get hover information (type, docs) for a symbol',
inputSchema: {
type: 'object',
properties: {
uri: {
type: 'string',
description: 'File URI'
},
line: {
type: 'number',
description: 'Line number (0-indexed)'
},
character: {
type: 'number',
description: 'Character offset (0-indexed)'
}
},
required: ['uri', 'line', 'character']
}
}
]
};
}
private async handleToolCall(params: any): Promise<any> {
const { name, arguments: args } = params;
switch (name) {
case 'vscode_ls_get_symbols':
return await this.tools.getSymbols(args.uri);
case 'vscode_ls_find_references':
return await this.tools.findReferences(
args.uri,
args.line,
args.character
);
case 'vscode_ls_rename':
return await this.tools.rename(
args.uri,
args.line,
args.character,
args.newName
);
case 'vscode_ls_get_definition':
return await this.tools.getDefinition(
args.uri,
args.line,
args.character
);
case 'vscode_ls_get_hover':
return await this.tools.getHover(
args.uri,
args.line,
args.character
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}
}