Skip to main content
Glama
index.ts•10.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { listDirectory, readFileContent, normalizeAndValidatePath } from './fs-utils.js'; import { ServerConfig, FileSystemItem } from './types.js'; import { getMimeType } from './mime-types.js'; import { searchByPath, searchContent } from './search-utils.js'; import { ResourceManager } from './resource-manager.js'; import { createLogger } from './logger.js'; import { MetricsCollector } from './metrics.js'; import * as path from 'path'; /** * Automatically detects the project root by finding the parent directory * of node_modules. This ensures the MCP server only accesses files within * the project where it's installed, preventing security vulnerabilities. * * @returns The absolute path to the project root * @throws Error if not running from within node_modules */ export function findProjectRoot(): string { // MCP servers always run from node_modules when properly installed const nodeModulesIndex = __dirname.lastIndexOf(`${path.sep}node_modules${path.sep}`); if (nodeModulesIndex === -1) { throw new Error( 'peekaboo-mcp must be run as an installed npm package. ' + 'Direct execution is not supported for security reasons.' ); } // Extract project root (everything before node_modules) return __dirname.substring(0, nodeModulesIndex); } const DEFAULT_CONFIG: ServerConfig = { recursive: true, maxDepth: 10, timeout: 30000, // 30 seconds default timeout maxFileSize: 10 * 1024 * 1024, // 10MB default max file size maxTotalSize: 100 * 1024 * 1024 // 100MB default max total size }; const serverStartTime = Date.now(); export function createPeekabooServer(rootDir: string, config: ServerConfig = DEFAULT_CONFIG) { const serverConfig = { ...DEFAULT_CONFIG, ...config }; const metrics = new MetricsCollector(); // Create resource manager with configured limits const resourceManager = new ResourceManager({ timeout: serverConfig.timeout, maxFileSize: serverConfig.maxFileSize, maxTotalSize: serverConfig.maxTotalSize }); const server = new Server( { name: 'peekaboo-mcp', version: '2.0.0', }, { capabilities: { resources: {}, tools: {} } } ); server.setRequestHandler(ListResourcesRequestSchema, async () => { const metric = metrics.startOperation('list_resources'); try { // Reset size tracking for new request resourceManager.resetSize(); const listOperation = listDirectory( rootDir, '.', serverConfig.recursive, serverConfig.maxDepth, 0, resourceManager ); const items = await resourceManager.withTimeout( listOperation, 'listDirectory' ); // Flatten the recursive structure for MCP resources const flattenItems = (items: FileSystemItem[], result: any[] = []): any[] => { for (const item of items) { const fullPath = `${rootDir}${item.path}`; result.push({ uri: `file://${fullPath}`, name: item.path, mimeType: item.type === 'file' ? getMimeType(item.name) : 'inode/directory', metadata: { type: item.type, size: item.size, hasChildren: item.children && item.children.length > 0 } }); if (item.children) { flattenItems(item.children, result); } } return result; }; const result = { resources: flattenItems(items) }; metrics.endOperation(metric, true); return result; } catch (error) { metrics.endOperation(metric, false, error instanceof Error ? error.message : 'Unknown error'); throw new McpError( ErrorCode.InternalError, `Failed to list resources: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; if (!uri.startsWith('file://')) { throw new McpError( ErrorCode.InvalidRequest, 'Only file:// URIs are supported' ); } const requestedPath = uri.slice(7); try { const validPath = normalizeAndValidatePath(rootDir, requestedPath); const readOperation = readFileContent(validPath, resourceManager); const content = await resourceManager.withTimeout( readOperation, 'readFileContent' ); return { contents: [{ uri: request.params.uri, mimeType: getMimeType(validPath), text: content }] }; } catch (error) { if (error instanceof Error && error.message.includes('outside root')) { throw new McpError( ErrorCode.InvalidRequest, 'Access denied: Path traversal attempt detected' ); } throw new McpError( ErrorCode.InternalError, `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_path', description: 'Search for files and directories by name pattern', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Search pattern (supports * and ** wildcards, e.g., "*.js", "**/test/*.json")' } }, required: ['pattern'] } }, { name: 'search_content', description: 'Search for content within files', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Text to search for in file contents' }, include: { type: 'string', description: 'Optional file pattern to search in (e.g., "*.js", "*.md")' }, ignoreCase: { type: 'boolean', description: 'Case-insensitive search (default: true)' } }, required: ['query'] } }, { name: 'health_check', description: 'Get server health status and metrics', inputSchema: { type: 'object', properties: {} } } ] }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === 'search_path') { const pattern = args?.pattern as string; if (!pattern) { throw new McpError(ErrorCode.InvalidRequest, 'Pattern is required'); } const searchOperation = searchByPath(rootDir, pattern); const paths = await resourceManager.withTimeout( searchOperation, 'searchByPath' ); return { content: [ { type: 'text', text: paths.length > 0 ? `Found ${paths.length} matches:\n${paths.join('\n')}` : 'No files found matching the pattern' } ] }; } if (name === 'search_content') { const query = args?.query as string; if (!query) { throw new McpError(ErrorCode.InvalidRequest, 'Query is required'); } const searchOperation = searchContent(rootDir, query, { include: args?.include as string, ignoreCase: args?.ignoreCase !== false, maxResults: 20 }); const results = await resourceManager.withTimeout( searchOperation, 'searchContent' ); if (results.length === 0) { return { content: [ { type: 'text', text: 'No matches found' } ] }; } let output = `Found matches in ${results.length} files:\n\n`; for (const result of results) { output += `đź“„ ${result.path}\n`; if (result.matches) { for (const match of result.matches) { output += ` Line ${match.line}: ${match.content}\n`; } } output += '\n'; } return { content: [ { type: 'text', text: output } ] }; } if (name === 'health_check') { const uptime = Date.now() - serverStartTime; const metricsData = metrics.getMetrics(); return { content: [ { type: 'text', text: JSON.stringify({ status: 'healthy', version: '2.0.0', uptime: uptime, uptimeHuman: `${Math.floor(uptime / 1000)}s`, metrics: metricsData.summary }, null, 2) } ] }; } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } catch (error) { if (error instanceof McpError) throw error; throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); return server; } async function main() { const logger = createLogger('peekaboo-mcp'); try { const rootDir = findProjectRoot(); const config: ServerConfig = { recursive: process.env.PEEKABOO_RECURSIVE !== 'false', maxDepth: process.env.PEEKABOO_MAX_DEPTH ? parseInt(process.env.PEEKABOO_MAX_DEPTH) : 10 }; const server = createPeekabooServer(rootDir, config); const transport = new StdioServerTransport(); await server.connect(transport); logger.info(`Peekaboo MCP server started with root: ${rootDir} (recursive: ${config.recursive}, maxDepth: ${config.maxDepth})`); } catch (error) { logger.error('Failed to start server:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } } if (require.main === module) { const logger = createLogger('peekaboo-mcp'); main().catch(error => { logger.error('Server error:', error); process.exit(1); }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/davstr1/peekabooMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server