mcp-repl
Version:
MCP REPL with code execution and semantic code search
665 lines (572 loc) • 19.3 kB
JavaScript
// Simple Node.js direct executor server using MCP SDK
import * as path from 'node:path';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
// Handle version flag
if (process.argv.includes('--version') || process.argv.includes('-v')) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
console.log(pkg.version);
process.exit(0);
}
// Handle help flag
if (process.argv.includes('--help') || process.argv.includes('-h')) {
console.log('MCP REPL - Code execution and semantic search server');
console.log('Usage: mcp-repl [working-directory]');
console.log('Options:');
console.log(' --version, -v Show version');
console.log(' --help, -h Show help');
process.exit(0);
}
import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { spawn } from 'child_process';
// Lazy load vector indexer to avoid startup issues
let vectorIndexer = null;
const getVectorIndexer = async () => {
if (!vectorIndexer) {
vectorIndexer = await import('./js-vector-indexer.js');
}
return vectorIndexer;
};
// Get the working directory from command line or use current directory
const workingDir = process.argv[2] && !process.argv[2].startsWith('-')
? path.resolve(process.argv[2])
: process.cwd();
// Initialize the MCP server
const server = new McpServer(
{
name: "direct-node-executor",
version: "1.0.0",
},
{
capabilities: {
tools: {}, // Enable tool support
},
}
);
// Initialize code search on startup
(async function initCodeSearch() {
try {
const { initialize, syncIndex } = await getVectorIndexer();
await initialize();
// Perform initial indexing of the working directory
await syncIndex([workingDir]);
} catch (error) {
// Silently handle errors
}
})();
// List available tools
const listToolsHandler = async () => {
return {
tools: [
{
name: "executenodejs",
description: "Execute JavaScript code directly with Node.js - supports ESM imports and all Node.js features",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
description: "JavaScript code to execute"
},
timeout: {
type: "number",
description: "Optional timeout in milliseconds (default: 120000)"
}
},
required: ["code"]
}
},
{
name: "executedeno",
description: "Execute JavaScript/TypeScript code with Deno - supports ESM imports and all Deno features",
inputSchema: {
type: "object",
properties: {
code: {
type: "string",
description: "JavaScript/TypeScript code to execute"
},
timeout: {
type: "number",
description: "Optional timeout in milliseconds (default: 120000)"
}
},
required: ["code"]
}
},
{
name: "searchcode",
description: "Semantic code search with metadata extraction and AST-aware chunking",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Semantic search query for code"
},
folders: {
type: "string",
description: "Optional comma-separated list of folders to search (defaults to working directory)"
},
extensions: {
type: "string",
description: "Optional comma-separated list of file extensions to include (default: js,ts)"
},
ignores: {
type: "string",
description: "Optional comma-separated list of patterns to ignore (default: node_modules)"
},
topK: {
type: "number",
description: "Optional number of results to return (default: 8)"
}
},
required: ["query"]
}
}
],
};
};
// Execute code function - simplified to pipe code into Node instead of using temp files
const executeCode = async (code, timeout = 120000) => {
const startTime = Date.now();
try {
// More robust detection if the code is likely CJS or ESM
// Look for explicit CJS markers: require, module.exports, __dirname, __filename
const cjsMarkers = [
'require(',
'module.exports',
'__dirname',
'__filename',
'exports.'
];
// Check if the code contains any CJS markers
const isCjs = cjsMarkers.some(marker => code.includes(marker));
// If code is CJS, wrap it with the appropriate CommonJS wrapper
if (isCjs) {
// For CommonJS, create a temporary file since piping with --input-type=commonjs
// doesn't work reliably in all Node.js versions
const fs = await import('fs');
const tempDir = path.join(workingDir, 'temp');
// Create temp directory if it doesn't exist
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Create a unique temporary file for CJS execution
const tempFile = path.join(tempDir, `node-exec-${Date.now()}-${Math.random().toString(36).substring(2)}.cjs`);
// Write the code to the temp file
fs.writeFileSync(tempFile, code, 'utf8');
return new Promise((resolve) => {
// Execute the file directly instead of piping for CJS
const nodeProcess = spawn('node', [tempFile], {
cwd: workingDir,
timeout,
env: process.env
});
let stdout = '';
let stderr = '';
nodeProcess.stdout.on('data', (data) => {
stdout += data;
});
nodeProcess.stderr.on('data', (data) => {
stderr += data;
});
nodeProcess.on('close', (code) => {
// Calculate execution time
const executionTimeMs = Date.now() - startTime;
// Clean up temporary file
try {
fs.unlinkSync(tempFile);
} catch (err) {
// Silently handle cleanup errors
}
resolve({
success: code === 0,
stdout,
stderr,
executionTimeMs,
code
});
});
nodeProcess.on('error', (err) => {
// Clean up temporary file
try {
fs.unlinkSync(tempFile);
} catch (cleanupErr) {
// Silently handle cleanup errors
}
resolve({
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime
});
});
});
} else {
// For ESM code, continue using stdin piping
return new Promise((resolve) => {
// Spawn Node.js process with stdin piping for ESM
const nodeProcess = spawn('node', ['--input-type=module'], {
cwd: workingDir,
timeout,
env: process.env
});
let stdout = '';
let stderr = '';
nodeProcess.stdout.on('data', (data) => {
stdout += data;
});
nodeProcess.stderr.on('data', (data) => {
stderr += data;
});
nodeProcess.on('close', (code) => {
// Calculate execution time
const executionTimeMs = Date.now() - startTime;
resolve({
success: code === 0,
stdout,
stderr,
executionTimeMs,
code
});
});
nodeProcess.on('error', (err) => {
resolve({
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime
});
});
// Write code to stdin and close
nodeProcess.stdin.write(code);
nodeProcess.stdin.end();
});
}
} catch (err) {
return {
success: false,
error: err.message
};
}
};
// Execute code with Deno - Refactored to pipe code via stdin and always use --allow-all
const executeDenoCode = async (code, timeout = 120000) => {
const startTime = Date.now();
try {
return new Promise((resolve) => {
// Build Deno command with --allow-all and stdin reading
const denoArgs = ['run', '--allow-all', '-']; // '-' tells Deno to read from stdin
// Execute with Deno
const denoProcess = spawn('deno', denoArgs, {
cwd: workingDir,
timeout,
env: process.env,
stdio: ['pipe', 'pipe', 'pipe'] // Ensure stdio streams are piped
});
let stdout = '';
let stderr = '';
denoProcess.stdout.on('data', (data) => {
stdout += data;
});
denoProcess.stderr.on('data', (data) => {
stderr += data;
});
denoProcess.on('close', (code) => {
// Calculate execution time
const executionTimeMs = Date.now() - startTime;
resolve({
success: code === 0,
stdout,
stderr,
executionTimeMs,
code
});
});
denoProcess.on('error', (err) => {
resolve({
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime
});
});
// Write code to stdin and close
denoProcess.stdin.write(code);
denoProcess.stdin.end();
});
} catch (err) {
return {
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime // Ensure time is recorded even on early catch
};
}
};
// Handle code search requests
const performCodeSearch = async (query, folders, extensions, ignores, topK) => {
const startTime = Date.now();
try {
// Default to working directory if no folders provided
const searchFolders = folders
? folders.split(',').map(f => path.resolve(f.trim()))
: [workingDir];
// Parse extensions and ignores
const searchExts = extensions
? extensions.split(',').map(e => e.trim().replace(/^\./, ''))
: ['js', 'ts'];
const searchIgnores = ignores
? ignores.split(',').map(i => i.trim())
: ['node_modules'];
// Sync index with current file system
const { syncIndex } = await getVectorIndexer();
await syncIndex(searchFolders, searchExts, searchIgnores);
// Run the query
const { queryIndex } = await getVectorIndexer();
const results = await queryIndex(query, topK || 8);
// Format results for enhanced display
const enhancedResults = results.map(result => {
// Create base result with standard fields
const enhancedResult = {
score: result.score,
type: result.type,
name: result.name,
qualifiedName: result.qualifiedName,
location: {
file: result.file,
startLine: result.startLine,
endLine: result.endLine,
lines: result.lines
},
documentation: result.doc || '',
preview: result.code
};
// Add structural metadata if available
if (result.structure) {
enhancedResult.structure = result.structure;
}
// Add relationships if available
if (result.relationships) {
enhancedResult.relationships = result.relationships;
}
return enhancedResult;
});
// Calculate execution time
const executionTimeMs = Date.now() - startTime;
return {
success: true,
results: enhancedResults,
executionTimeMs,
searchFolders,
searchExts,
searchIgnores,
metadata: {
structuralSearch: true,
indexedTypes: ['file', 'function', 'class', 'method', 'property', 'import', 'export']
}
};
} catch (error) {
return {
success: false,
error: error.message,
executionTimeMs: Date.now() - startTime
};
}
};
// Handle code execution requests
const callToolHandler = async (request) => {
try {
const { name, arguments: args = {} } = request.params;
// Handle Node.js execution
if (name === 'executenodejs' || name === 'execute' || name === 'mcp_mcp_repl_execute') {
const { code, timeout = 120000 } = args;
if (!code) {
throw new Error("Missing code argument for execute tool");
}
// Execute the code with Node.js (ignore any workingDir passed in args)
const result = await executeCode(code, timeout);
// Create content array with output
const outputLines = [];
// Add stdout if any
if (result.stdout) {
outputLines.push({
type: 'text',
text: result.stdout.trim()
});
}
// Add stderr if any
if (result.stderr) {
outputLines.push({
type: 'text',
text: `ERROR: ${result.stderr.trim()}`
});
}
// Add error message if execution failed
if (!result.success && result.error) {
outputLines.push({
type: 'text',
text: `ERROR: ${result.error}`
});
}
// Add execution summary
outputLines.push({
type: 'text',
text: `Execution completed in ${result.executionTimeMs}ms with exit code ${result.code || 0}`
});
return {
content: outputLines
};
}
// Handle Deno execution
if (name === 'executedeno' || name === 'mcp_mcp_repl_executedeno') {
const { code, timeout = 120000 } = args;
if (!code) {
throw new Error("Missing code argument for Deno execute tool");
}
// Execute the code with Deno (ignore any workingDir passed in args)
const result = await executeDenoCode(code, timeout);
// Create content array with output
const outputLines = [];
// Add stdout if any
if (result.stdout) {
outputLines.push({
type: 'text',
text: result.stdout.trim()
});
}
// Add stderr if any
if (result.stderr) {
outputLines.push({
type: 'text',
text: `ERROR: ${result.stderr.trim()}`
});
}
// Add error message if execution failed
if (!result.success && result.error) {
outputLines.push({
type: 'text',
text: `ERROR: ${result.error}`
});
}
// Add execution summary
outputLines.push({
type: 'text',
text: `Deno execution completed in ${result.executionTimeMs}ms with exit code ${result.code || 0}`
});
return {
content: outputLines
};
}
// Handle code search
if (name === 'searchcode' || name === 'mcp_mcp_repl_searchcode') {
const { query, folders, extensions, ignores, topK } = args;
if (!query) {
throw new Error("Missing query argument for code search tool");
}
// Perform code search
const result = await performCodeSearch(query, folders, extensions, ignores, topK);
// Create content array with output
const outputLines = [];
if (result.success) {
// Add a header summarizing search configuration
outputLines.push({
type: 'text',
text: `Code search for "${query}"\nSearched in: ${result.searchFolders.join(', ')}\nIncluded extensions: ${result.searchExts.join(', ')}\nIgnored patterns: ${result.searchIgnores.join(', ')}`
});
if (result.results.length === 0) {
outputLines.push({
type: 'text',
text: 'No results found.'
});
} else {
// Add formatted results
outputLines.push({
type: 'text',
text: `Found ${result.results.length} result(s):`
});
// Add each result
for (const res of result.results) {
const title = `[${res.score}] ${res.file}:${res.startLine}-${res.endLine} - ${res.type} ${res.qualifiedName}`;
let details = [];
if (res.parameters) details.push(`Parameters: ${res.parameters}`);
if (res.parameterTypes) details.push(`Parameter types: ${res.parameterTypes}`);
if (res.returnType) details.push(`Return type: ${res.returnType}`);
if (res.parentClass) details.push(`Parent class: ${res.parentClass}`);
if (res.extends) details.push(`Extends: ${res.extends}`);
if (res.doc) details.push(`Doc: ${res.doc}`);
if (res.controlFlow && res.controlFlow.length > 0) {
details.push(`Contains: ${res.controlFlow.join(', ')}`);
}
details.push(`Lines: ${res.lines}, Tokens: ${res.tokens}`);
if (res.code) details.push(`Code snippet: ${res.code}`);
outputLines.push({
type: 'text',
text: `${title}\n${details.join('\n')}`
});
}
}
// Add execution summary
outputLines.push({
type: 'text',
text: `Search completed in ${result.executionTimeMs}ms`
});
} else {
// Add error message if search failed
outputLines.push({
type: 'text',
text: `ERROR: ${result.error}`
});
}
return {
content: outputLines
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
return {
content: [
{
type: 'text',
text: `ERROR: ${error.message}`
}
]
};
}
};
// Register handlers
server.setRequestHandler(ListToolsRequestSchema, listToolsHandler);
server.setRequestHandler(CallToolRequestSchema, callToolHandler);
// Global error handlers that are silent to keep stdio clean
process.on('uncaughtException', (err) => {
// Silently handle uncaught exceptions
});
process.on('unhandledRejection', (reason) => {
// Silently handle unhandled rejections
});
// Start the server
async function main() {
try {
// Create transport and connect
const stdioTransport = new StdioServerTransport();
await server.connect(stdioTransport);
// Add a keep-alive mechanism to prevent the process from exiting
setInterval(() => {
// This is a no-op interval that keeps the Node.js event loop active
}, 60000);
// Also handle the SIGINT signal explicitly
process.on('SIGINT', () => {
process.exit(0);
});
} catch (error) {
// Only log critical startup errors that prevent the server from running
console.error(`Error starting server: ${error.message}`);
process.exit(1);
}
}
// Run the server
main();