Krep MCP Server
by bmorphism
Verified
// MCP-compliant server for krep
const { exec } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
// Set up error handling for uncaught exceptions
process.on('uncaughtException', error => {
console.error(`[MCP Server] Uncaught exception: ${error.message}`);
console.error(`[MCP Server] Stack trace: ${error.stack}`);
// Don't exit the process, just log the error
});
// Set up error handling for unhandled promise rejections
process.on('unhandledRejection', reason => {
console.error(`[MCP Server] Unhandled promise rejection: ${reason}`);
// Don't exit the process, just log the error
});
// Determine optimal thread count based on available CPU cores
function getOptimalThreadCount() {
// Get the number of CPU cores available
const cpuCount = os.cpus().length;
// Use all available cores (can be adjusted as needed)
// Some strategies use cpuCount - 1 to leave a core for the OS
return cpuCount;
}
// Find the krep binary
function findKrepBinary() {
// Get the full path to the node executable directory
const nodeDirectory = path.dirname(process.execPath);
// Try multiple possible paths for the krep binary - ensure all are absolute
const possiblePaths = [
// Project-specific directories
path.resolve(__dirname, '../../krep-native/krep'), // Relative to project directory
path.resolve(__dirname, '../krep-native/krep'), // Alternative relative path
// Standard installation locations
'/usr/local/bin/krep', // Standard installation
'/opt/homebrew/bin/krep', // Homebrew on Apple Silicon
// Look near node executable
path.join(nodeDirectory, 'krep'),
// Home directory options
path.join(process.env.HOME || '', 'krep-native/krep'),
path.join(process.env.HOME || '', 'bin/krep'),
];
// Always log the paths being searched to help with debugging
console.error('Looking for krep binary in:');
// Try each path and return the first one that exists
for (const p of possiblePaths) {
const exists = fs.existsSync(p);
console.error(`- ${p} (${exists ? 'found' : 'not found'})`);
if (exists) {
return p;
}
}
// If KREP_PATH is set in environment, use that even if it doesn't exist
// This allows for testing and development scenarios
if (process.env.KREP_PATH) {
console.error(`Using KREP_PATH from environment: ${process.env.KREP_PATH}`);
return process.env.KREP_PATH;
}
return null; // Return null if no binary found
}
// Path to the krep binary - allow it to be set via environment variable
const KREP_PATH =
process.env.KREP_PATH || findKrepBinary() || path.join(__dirname, '../../krep-native/krep');
console.error(`[MCP Server] Using krep binary at: ${KREP_PATH}`);
// MCP JSON-RPC server
class KrepMcpServer {
constructor() {
// Store start time for performance logging
this.startTime = Date.now();
// Log server initialization
console.error(`[MCP Server] Initializing krep-mcp-server at ${new Date().toISOString()}`);
console.error(`[MCP Server] Node version: ${process.version}`);
console.error(`[MCP Server] Working directory: ${process.cwd()}`);
// Check if krep binary exists, unless we're in test mode
if (!fs.existsSync(KREP_PATH) && !process.env.KREP_SKIP_CHECK) {
const errorMessage = `Error: krep binary not found at ${KREP_PATH}`;
console.error(`[MCP Server] ${errorMessage}`);
console.error('[MCP Server] Please install krep or set KREP_PATH environment variable');
// In production mode, exit. In test mode with KREP_TEST_MODE, continue.
if (!process.env.KREP_TEST_MODE) {
this.sendLogMessage('error', {
message: errorMessage,
binaryPath: KREP_PATH,
cwd: process.cwd(),
env: {
HOME: process.env.HOME,
PATH: process.env.PATH,
// Only include non-sensitive variables
},
});
// Exit after a short delay to allow log message to be processed
setTimeout(() => process.exit(1), 100);
return;
}
console.error('[MCP Server] Running in test mode, continuing despite missing krep binary');
} else {
console.error(`[MCP Server] Using krep binary at: ${KREP_PATH}`);
}
this.functions = {
krep: this.krepFunction.bind(this),
};
this.initialized = false;
this.handleInput();
}
// Main handler for stdin/stdout communication
handleInput() {
console.error('[MCP Server] Setting up stdin/stdout handlers');
// For MCP Inspector compatibility, use raw Buffer chunks for reliable binary handling
process.stdin.setEncoding('utf8');
// Use buffer for UTF-8 safe accumulation
let buffer = '';
process.stdin.on('data', chunk => {
// Log chunk details with clear identifier
console.error(`[MCP Server] Received chunk of ${chunk.length} bytes`);
if (process.env.DEBUG) {
console.error(
`[MCP Server] Chunk preview: ${chunk.substring(0, Math.min(50, chunk.length))}`
);
}
// Append the chunk to our buffer
buffer += chunk;
try {
// Look for complete JSON-RPC messages
const messages = this.extractMessages(buffer);
if (messages.extracted.length > 0) {
// Update buffer and process messages
buffer = messages.remainingBuffer;
console.error(
`[MCP Server] Processing ${messages.extracted.length} message(s), ${buffer.length} bytes remaining in buffer`
);
for (const message of messages.extracted) {
this.processMessage(message);
}
}
} catch (error) {
// Log properly and recover
console.error(`[MCP Server] Error processing input: ${error.message}`);
console.error(`[MCP Server] Stack trace: ${error.stack}`);
this.sendLogMessage('error', { message: 'Error processing input', error: error.message });
this.sendErrorResponse(null, `Error processing request: ${error.message}`);
// Attempt to recover by clearing buffer if it's growing too large
if (buffer.length > 10000) {
console.error('[MCP Server] Buffer too large, clearing for recovery');
buffer = '';
}
}
});
// Handle stdin closing
process.stdin.on('end', () => {
console.error('[MCP Server] stdin stream ended, shutting down');
this.sendLogMessage('info', 'Server shutting down due to stdin close');
// Don't exit the process, just log the event
console.error('[MCP Server] Not exiting process despite stdin close');
});
// Handle errors
process.stdin.on('error', error => {
console.error(`[MCP Server] stdin error: ${error.message}`);
this.sendLogMessage('error', { message: 'stdin error', error: error.message });
// Don't exit the process, just log the error
console.error('[MCP Server] Not exiting process despite stdin error');
});
}
// Extract complete JSON messages from buffer
extractMessages(buffer) {
console.error(`[MCP Server] Processing buffer of length: ${buffer.length}`);
if (buffer.length > 0) {
console.error(
`[MCP Server] Buffer preview: ${buffer.substring(0, Math.min(50, buffer.length))}`
);
}
const extracted = [];
let startIdx = 0;
// First, try to parse as direct JSON if it looks like JSON
if (buffer.startsWith('{') && buffer.includes('"method"')) {
try {
// Try to parse the entire buffer as a single JSON message
const message = JSON.parse(buffer);
console.error('[MCP Server] Successfully parsed direct JSON message');
extracted.push(message);
return {
extracted,
remainingBuffer: '',
};
} catch (error) {
console.error(`[MCP Server] Failed to parse direct JSON: ${error.message}`);
// Continue with header-based parsing
}
}
while (startIdx < buffer.length) {
// Look for Content-Length header with multiple possible formats
// 1. Standard format with \r\n\r\n
// 2. Alternative format with just \n\n
// 3. Single line format with just \n
let headerMatch = buffer.slice(startIdx).match(/Content-Length:\s*(\d+)\r\n\r\n/);
if (!headerMatch) {
headerMatch = buffer.slice(startIdx).match(/Content-Length:\s*(\d+)\n\n/);
}
if (!headerMatch) {
headerMatch = buffer.slice(startIdx).match(/Content-Length:\s*(\d+)\n/);
}
if (!headerMatch) {
// No complete header found, wait for more data
console.error('[MCP Server] No complete Content-Length header found in buffer');
// If the buffer looks like it might be a direct JSON message, try to parse it
if (buffer.startsWith('{') && buffer.endsWith('}')) {
try {
const message = JSON.parse(buffer);
console.error('[MCP Server] Successfully parsed direct JSON message');
extracted.push(message);
return {
extracted,
remainingBuffer: '',
};
} catch (error) {
console.error(`[MCP Server] Failed to parse as direct JSON: ${error.message}`);
}
}
break;
}
// Calculate where header ends and content begins
const headerMatchLength = headerMatch[0].length;
const headerMatchStart = startIdx + headerMatch.index;
const contentStart = headerMatchStart + headerMatchLength;
// Parse the content length
const contentLength = parseInt(headerMatch[1], 10);
console.error(`[MCP Server] Found header: Content-Length: ${contentLength}`);
// Check if we have the complete content
if (buffer.length < contentStart + contentLength) {
console.error(
`[MCP Server] Incomplete message: have ${buffer.length - contentStart} of ${contentLength} bytes`
);
break;
}
// Extract and parse the JSON content
const jsonContent = buffer.slice(contentStart, contentStart + contentLength);
try {
// Make sure we parse the content as a complete block
const jsonStr = jsonContent.toString('utf8');
const message = JSON.parse(jsonStr);
extracted.push(message);
console.error('[MCP Server] Successfully parsed message');
} catch (error) {
console.error(`[MCP Server] Failed to parse JSON message: ${error.message}`);
console.error(`[MCP Server] Problematic content: ${jsonContent.substring(0, 100)}`);
}
// Move past this message
startIdx = contentStart + contentLength;
}
return {
extracted,
remainingBuffer: buffer.slice(startIdx),
};
}
// Process an incoming message
processMessage(message) {
// Ensure all logging goes to stderr only
console.error(`[MCP Server] Received message: ${JSON.stringify(message)}`);
if (message.method === 'initialize') {
console.error('[MCP Server] Handling initialize message...');
this.handleInitialize(message);
console.error('[MCP Server] Initialize handler completed');
} else if (this.initialized && message.method === 'executeFunction') {
console.error('[MCP Server] Handling executeFunction message...');
this.handleExecuteFunction(message);
} else {
console.error(`[MCP Server] Unknown method: ${message.method}`);
this.sendErrorResponse(message.id, `Unknown or unsupported method: ${message.method}`);
}
}
// Handle initialize method
handleInitialize(message) {
this.initialized = true;
const capabilities = {
functions: [
{
name: 'krep',
description: 'Unified function for pattern searching in files or strings',
parameters: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Pattern to search for',
},
target: {
type: 'string',
description: 'File path or string to search in',
},
mode: {
type: 'string',
description: 'Search mode: "file" (default), "string", or "count"',
enum: ['file', 'string', 'count'],
},
caseSensitive: {
type: 'boolean',
description: 'Case-sensitive search (default: true)',
},
threads: {
type: 'integer',
description: `Number of threads to use (default: auto-detected based on CPU cores, currently ${getOptimalThreadCount()})`,
},
},
required: ['pattern', 'target'],
},
},
],
};
this.sendResponse(message.id, { capabilities });
}
// Handle executeFunction method
handleExecuteFunction(message) {
const { function: functionName, parameters } = message.params;
if (!this.functions[functionName]) {
return this.sendErrorResponse(message.id, `Function not found: ${functionName}`);
}
try {
this.functions[functionName](parameters, message.id);
} catch (error) {
this.sendErrorResponse(message.id, `Error executing function: ${error.message}`);
}
}
// Unified krep function
krepFunction(params, id) {
const { pattern, target, mode = 'file', caseSensitive = true } = params;
const threads = params.threads !== undefined ? params.threads : getOptimalThreadCount();
console.error(
`[MCP Server] krep called with pattern: ${pattern}, target: ${target}, mode: ${mode}`
);
if (!pattern || !target) {
console.error('[MCP Server] Missing required parameters');
return this.sendErrorResponse(id, 'Missing required parameters: pattern and target');
}
// Build command based on mode
const caseFlag = caseSensitive ? '' : '-i';
const threadFlag = `-t ${threads}`;
let command = '';
if (mode === 'string') {
// String search mode
command = `${KREP_PATH} ${caseFlag} ${threadFlag} -s "${pattern}" "${target}"`;
} else if (mode === 'count') {
// Count mode
command = `${KREP_PATH} ${caseFlag} ${threadFlag} -c "${pattern}" "${target}"`;
} else {
// Default file search mode
command = `${KREP_PATH} ${caseFlag} ${threadFlag} "${pattern}" "${target}"`;
}
console.error(`[MCP Server] Executing command: ${command}`);
// Return a mock response for testing mode
if (process.env.KREP_TEST_MODE) {
console.error('[MCP Server] In test mode, returning mock response');
this.sendResponse(id, {
pattern,
target,
mode,
results: `Found 5 matches for "${pattern}" in ${target}`,
performance: {
matchCount: 5,
searchTime: 0.001,
searchSpeed: 100,
algorithmUsed: this.getAlgorithmInfo(pattern),
threads,
caseSensitive,
},
success: true,
});
return;
}
// Handle the case where the krep binary doesn't exist
if (!fs.existsSync(KREP_PATH) && !process.env.KREP_SKIP_CHECK) {
console.error(`[MCP Server] krep binary not found at ${KREP_PATH}`);
return this.sendErrorResponse(id, `krep binary not found at ${KREP_PATH}`);
}
exec(command, { maxBuffer: 1024 * 1024 * 10 }, (error, stdout, stderr) => {
if (error) {
console.error(`[MCP Server] Error executing krep: ${error.message}`);
console.error(`[MCP Server] stderr: ${stderr}`);
// For file not found or permission errors, still return a valid response
if (
error.message.includes('No such file') ||
error.message.includes('Permission denied') ||
error.message.includes('not found') ||
error.message.includes('cannot access')
) {
console.error('[MCP Server] Handling file access error gracefully');
this.sendResponse(id, {
pattern,
target,
mode,
results: `No matches found (${error.message})`,
performance: {
matchCount: 0,
searchTime: 0,
searchSpeed: 0,
algorithmUsed: this.getAlgorithmInfo(pattern),
threads,
caseSensitive,
},
success: true,
});
return;
}
return this.sendErrorResponse(id, error.message, stderr);
}
console.error(`[MCP Server] krep executed successfully, stdout length: ${stdout.length}`);
// Extract performance metrics from output
const matchCountMatch = stdout.match(/Found (\d+) matches/);
const timeMatch = stdout.match(/Search completed in ([\d.]+) seconds/);
const speedMatch = stdout.match(/([\d.]+) MB\/s/);
const algorithmMatch = stdout.match(/Using ([^\\n]+) algorithm/);
const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : 0;
const searchTime = timeMatch ? parseFloat(timeMatch[1]) : null;
const searchSpeed = speedMatch ? parseFloat(speedMatch[1]) : null;
const algorithmUsed = algorithmMatch
? algorithmMatch[1].trim()
: this.getAlgorithmInfo(pattern);
// Build response based on mode
const response = {
pattern,
target,
mode,
results: stdout,
performance: {
matchCount,
searchTime,
searchSpeed,
algorithmUsed,
threads,
caseSensitive,
},
success: true,
};
this.sendResponse(id, response);
});
}
// Get algorithm info based on pattern
getAlgorithmInfo(pattern) {
const patternLen = pattern.length;
if (patternLen < 3) {
return 'KMP (Knuth-Morris-Pratt) - Optimized for very short patterns';
} else if (patternLen > 16) {
return 'Rabin-Karp - Efficient for longer patterns with better hash distribution';
}
// Check if we're likely on a platform with SIMD support
const isAppleSilicon = process.platform === 'darwin' && process.arch === 'arm64';
const isModernX64 = process.platform !== 'darwin' && process.arch === 'x64';
if (isAppleSilicon) {
return 'NEON SIMD - Hardware-accelerated search on Apple Silicon';
} else if (isModernX64) {
return 'SSE4.2/AVX2 - Hardware-accelerated search with vector instructions';
}
return 'Boyer-Moore-Horspool - Efficient general-purpose string search';
}
// Send a JSON-RPC response
sendResponse(id, result) {
console.error('Sending response for id:', id);
const response = {
jsonrpc: '2.0',
id,
result,
};
this.sendMessage(response);
}
// Send a JSON-RPC error response
sendErrorResponse(id, message, data = null) {
console.error('Sending error response for id:', id, 'Message:', message);
const response = {
jsonrpc: '2.0',
id,
error: {
code: -32000,
message,
data,
},
};
this.sendMessage(response);
}
// Send a message following the JSON-RPC over stdin/stdout protocol
sendMessage(message) {
try {
// Use Buffer to ensure proper UTF-8 encoding for all characters (including emoji)
const jsonMessage = JSON.stringify(message);
const messageBuffer = Buffer.from(jsonMessage, 'utf8');
const contentLength = messageBuffer.length;
// Exactly Content-Length: N\r\n\r\n with no extra spaces
const header = `Content-Length: ${contentLength}\r\n\r\n`;
// Only log the header info to stderr, not stdout
console.error(`[MCP Server] Sending response with length: ${contentLength}`);
if (process.env.DEBUG) {
console.error(
`[MCP Server] Response preview: ${jsonMessage.substring(0, Math.min(100, jsonMessage.length))}`
);
}
// Write the header and content separately to avoid Buffer.concat issues
process.stdout.write(header);
process.stdout.write(jsonMessage);
// Flush stdout to ensure the message is sent immediately
if (typeof process.stdout.flush === 'function') {
process.stdout.flush();
}
} catch (error) {
console.error(`[MCP Server] Error sending message: ${error.message}`);
console.error(`[MCP Server] Stack trace: ${error.stack}`);
}
}
// Send a log message notification to the client
sendLogMessage(level, data) {
const message = {
jsonrpc: '2.0',
method: 'log',
params: {
level: level || 'info',
data: data || {},
},
};
this.sendMessage(message);
console.error(
`[MCP Server] Log message sent (${level}): ${typeof data === 'string' ? data : JSON.stringify(data)}`
);
}
}
// Start the server if this file is executed directly
if (require.main === module) {
new KrepMcpServer();
}
module.exports = KrepMcpServer;