cursor-mcp-server.js•10.7 kB
#!/usr/bin/env node
/**
* Cursor MCP Server
*
* Stellt Cursor-Tools als MCP Server bereit:
* - codebase_search: Semantische Suche im Codebase
* - file_read: Dateien lesen
* - file_list: Verzeichnisse auflisten
* - file_write: Dateien schreiben (optional)
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Workspace-Pfad aus Environment (default: aktuelles Verzeichnis)
const WORKSPACE_PATH = process.env.WORKSPACE_PATH || process.cwd();
const server = new Server(
{
name: process.env.MCP_SERVER_NAME || 'cursor-mcp',
version: process.env.MCP_SERVER_VERSION || '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Tools auflisten
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'codebase_search',
description: 'Führt eine semantische Suche im Codebase durch. Sucht nach Code, Funktionen, Klassen oder Konzepten basierend auf einer natürlichen Sprachanfrage.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Die Suchanfrage in natürlicher Sprache (z.B. "Wie wird die Patient-Suche implementiert?")',
},
target_directories: {
type: 'array',
items: { type: 'string' },
description: 'Optionale Liste von Verzeichnissen, in denen gesucht werden soll. Leer = gesamter Workspace',
},
},
required: ['query'],
},
},
{
name: 'file_read',
description: 'Liest den Inhalt einer Datei aus dem Workspace.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Relativer Pfad zur Datei (relativ zum Workspace-Root)',
},
offset: {
type: 'number',
description: 'Optionale Zeilennummer zum Starten (1-basiert)',
},
limit: {
type: 'number',
description: 'Optionale maximale Anzahl von Zeilen zum Lesen',
},
},
required: ['path'],
},
},
{
name: 'file_list',
description: 'Listet Dateien und Verzeichnisse in einem Verzeichnis auf.',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Relativer Pfad zum Verzeichnis (relativ zum Workspace-Root). Leer = Root',
},
recursive: {
type: 'boolean',
description: 'Soll rekursiv gelistet werden?',
default: false,
},
ignore_globs: {
type: 'array',
items: { type: 'string' },
description: 'Optionale Glob-Patterns zum Ignorieren (z.B. ["node_modules/**", "*.log"])',
},
},
required: ['directory'],
},
},
{
name: 'file_write',
description: 'Schreibt oder aktualisiert eine Datei im Workspace. Vorsicht: Kann bestehende Dateien überschreiben!',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Relativer Pfad zur Datei (relativ zum Workspace-Root)',
},
contents: {
type: 'string',
description: 'Der Inhalt, der in die Datei geschrieben werden soll',
},
},
required: ['path', 'contents'],
},
},
],
};
});
// Tool-Aufrufe verarbeiten
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'codebase_search': {
// Für echte semantische Suche bräuchtest du eine Vector-DB oder LLM
// Hier eine einfache Text-Suche als Fallback
const query = args.query || '';
const targetDirs = args.target_directories || [];
// Einfache Implementierung: Suche nach Dateien, die den Query-Text enthalten
// In Produktion: Nutze echte semantische Suche (z.B. über Cursor API)
const results = await searchInFiles(query, targetDirs);
return {
content: [
{
type: 'text',
text: JSON.stringify({
query,
results: results.slice(0, 10), // Top 10 Ergebnisse
total: results.length,
}, null, 2),
},
],
};
}
case 'file_read': {
const filePath = path.resolve(WORKSPACE_PATH, args.path);
// Sicherheitsprüfung: Nur innerhalb Workspace
if (!filePath.startsWith(path.resolve(WORKSPACE_PATH))) {
throw new Error('Pfad außerhalb des Workspace nicht erlaubt');
}
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n');
let output = content;
if (args.offset || args.limit) {
const start = (args.offset || 1) - 1;
const end = args.limit ? start + args.limit : lines.length;
output = lines.slice(start, end).join('\n');
}
return {
content: [
{
type: 'text',
text: output,
},
],
};
}
case 'file_list': {
const dirPath = args.directory
? path.resolve(WORKSPACE_PATH, args.directory)
: WORKSPACE_PATH;
// Sicherheitsprüfung
if (!dirPath.startsWith(path.resolve(WORKSPACE_PATH))) {
throw new Error('Pfad außerhalb des Workspace nicht erlaubt');
}
const items = await listDirectory(dirPath, args.recursive || false, args.ignore_globs || []);
return {
content: [
{
type: 'text',
text: JSON.stringify(items, null, 2),
},
],
};
}
case 'file_write': {
const filePath = path.resolve(WORKSPACE_PATH, args.path);
// Sicherheitsprüfung
if (!filePath.startsWith(path.resolve(WORKSPACE_PATH))) {
throw new Error('Pfad außerhalb des Workspace nicht erlaubt');
}
// Verzeichnis erstellen, falls nicht vorhanden
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, args.contents, 'utf-8');
return {
content: [
{
type: 'text',
text: `Datei erfolgreich geschrieben: ${args.path}`,
},
],
};
}
default:
throw new Error(`Unbekanntes Tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Fehler: ${error.message}`,
},
],
isError: true,
};
}
});
// Hilfsfunktionen
async function searchInFiles(query, targetDirs) {
const results = [];
const searchDirs = targetDirs.length > 0
? targetDirs.map(d => path.resolve(WORKSPACE_PATH, d))
: [WORKSPACE_PATH];
for (const dir of searchDirs) {
try {
const files = await getAllFiles(dir);
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf-8');
if (content.toLowerCase().includes(query.toLowerCase())) {
results.push({
file: path.relative(WORKSPACE_PATH, file),
matches: content.split('\n').filter(line =>
line.toLowerCase().includes(query.toLowerCase())
).length,
});
}
} catch (err) {
// Datei kann nicht gelesen werden (binär, etc.) - überspringen
}
}
} catch (err) {
// Verzeichnis existiert nicht - überspringen
}
}
return results;
}
async function getAllFiles(dir) {
const files = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Ignoriere node_modules, .git, etc.
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
files.push(...await getAllFiles(fullPath));
}
} else if (entry.isFile()) {
files.push(fullPath);
}
}
} catch (err) {
// Verzeichnis kann nicht gelesen werden
}
return files;
}
async function listDirectory(dirPath, recursive, ignoreGlobs) {
const items = [];
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relativePath = path.relative(WORKSPACE_PATH, fullPath);
// Ignore-Patterns prüfen
if (shouldIgnore(relativePath, ignoreGlobs)) {
continue;
}
if (entry.isDirectory()) {
items.push({
name: entry.name,
path: relativePath,
type: 'directory',
});
if (recursive) {
items.push(...await listDirectory(fullPath, true, ignoreGlobs));
}
} else {
items.push({
name: entry.name,
path: relativePath,
type: 'file',
});
}
}
} catch (err) {
// Verzeichnis kann nicht gelesen werden
}
return items;
}
function shouldIgnore(filePath, ignoreGlobs) {
// Einfache Glob-Implementierung (für Produktion: nutze minimatch oder ähnlich)
for (const pattern of ignoreGlobs) {
const regex = new RegExp(
pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\./g, '\\.')
);
if (regex.test(filePath)) {
return true;
}
}
return false;
}
// Server starten
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Cursor MCP Server gestartet');
}
main().catch((error) => {
console.error('Fehler beim Starten des MCP Servers:', error);
process.exit(1);
});