Skip to main content
Glama

Claude MCP Server Integration

by mokemoke0821
simple-server.js17.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { execSync } from 'child_process'; import fs, { createReadStream, promises as fsPromises } from 'fs'; import path from 'path'; import readline from 'readline'; class SimpleFileCommanderServer { constructor() { this.server = new Server( { name: 'enhanced_file_commander', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); // エラーハンドリング this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } // ファイルサイズのフォーマット formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 日付のフォーマット formatDate(date) { return new Date(date).toLocaleString('ja-JP', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // ディレクトリの一覧取得 async listFilesInDirectory(directory, recursive = false) { const result = []; const processDirectory = async (dir, relativeTo = null) => { const entries = await fsPromises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relativePath = relativeTo ? path.relative(relativeTo, fullPath) : null; if (entry.isDirectory()) { const stats = await fsPromises.stat(fullPath); result.push({ name: entry.name, path: fullPath, relativePath: relativePath, isDirectory: true, modifiedAt: stats.mtime }); if (recursive) { await processDirectory(fullPath, relativeTo || dir); } } else { const stats = await fsPromises.stat(fullPath); result.push({ name: entry.name, path: fullPath, relativePath: relativePath, isDirectory: false, size: stats.size, modifiedAt: stats.mtime }); } } }; await processDirectory(directory, recursive ? directory : null); return result; } // テキストファイルかどうかを判定する関数 isTextFile(filePath) { const ext = path.extname(filePath).toLowerCase(); // 一般的なテキストファイル拡張子のリスト const textExtensions = [ '.txt', '.md', '.js', '.ts', '.jsx', '.tsx', '.html', '.htm', '.css', '.scss', '.sass', '.less', '.json', '.xml', '.yaml', '.yml', '.csv', '.py', '.rb', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.go', '.rs', '.swift', '.kt', '.sh', '.bat', '.ps1', '.log', '.ini', '.conf', '.cfg', '.config' ]; return textExtensions.includes(ext); } // ファイル内容を検索する関数 async searchFileContent(filePath, pattern) { // テキストファイルチェック if (!this.isTextFile(filePath)) { return []; } try { const fileStream = createReadStream(filePath, { encoding: 'utf8' }); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); const regex = new RegExp(pattern, 'g'); const results = []; const maxContext = 3; const lines = []; let lineNumber = 0; // すべての行を読み込む for await (const line of rl) { lineNumber++; lines.push({ num: lineNumber, text: line }); } // マッチする行を検索 for (let i = 0; i < lines.length; i++) { const line = lines[i]; const match = regex.test(line.text); if (match) { // 正規表現のリセット regex.lastIndex = 0; // 前後のコンテキスト行を取得 const contextBefore = []; for (let j = Math.max(0, i - maxContext); j < i; j++) { contextBefore.push(lines[j].text); } const contextAfter = []; for (let j = i + 1; j < Math.min(lines.length, i + maxContext + 1); j++) { contextAfter.push(lines[j].text); } results.push({ file: filePath, line: line.num, content: line.text, context: { before: contextBefore, after: contextAfter } }); } } return results; } catch (error) { console.error(`Error searching in file ${filePath}:`, error); return [{ file: filePath, error: error.message }]; } } // ファイル比較関数 async compareFiles(file1Path, file2Path) { // ファイルの存在確認 if (!fs.existsSync(file1Path)) { throw new Error(`ファイル1 "${file1Path}" が存在しません`); } if (!fs.existsSync(file2Path)) { throw new Error(`ファイル2 "${file2Path}" が存在しません`); } // ファイルの内容を読み込み const content1 = await fsPromises.readFile(file1Path, 'utf8'); const content2 = await fsPromises.readFile(file2Path, 'utf8'); // ファイルが同一かどうかをチェック if (content1 === content2) { return { identical: true, differences: [], summary: `ファイルは同一です: "${path.basename(file1Path)}" と "${path.basename(file2Path)}"` }; } // 行ごとに分割して比較 const lines1 = content1.split(/\r?\n/); const lines2 = content2.split(/\r?\n/); const differences = []; const maxLines = Math.max(lines1.length, lines2.length); for (let i = 0; i < maxLines; i++) { const line1 = i < lines1.length ? lines1[i] : null; const line2 = i < lines2.length ? lines2[i] : null; if (line1 !== line2) { differences.push({ lineNumber: i + 1, file1Line: line1, file2Line: line2 }); } } // 差分の要約 const summary = `${differences.length} 行の差分が見つかりました。`; return { identical: false, differences, summary }; } // シェルコマンド実行 executeCommand(command, workingDirectory = process.cwd()) { try { const options = { cwd: workingDirectory, encoding: 'utf8', maxBuffer: 1024 * 1024 * 5 // 5MBの最大バッファ }; const output = execSync(command, options); return { success: true, output: output.toString() }; } catch (error) { return { success: false, error: error.message, stderr: error.stderr ? error.stderr.toString() : null }; } } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'hello_world', description: '簡単なテスト用ツール', inputSchema: { type: 'object', properties: { name: { type: 'string', description: '挨拶する相手の名前', }, }, required: ['name'], }, }, { name: 'execute_shell_command', description: 'シェルコマンドを実行します', inputSchema: { type: 'object', properties: { command: { type: 'string', description: '実行するコマンド', }, workingDirectory: { type: 'string', description: 'コマンドを実行するディレクトリ', } }, required: ['command'], }, }, { name: 'search_files', description: 'ファイル内容を検索します', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: '検索対象ディレクトリ', }, pattern: { type: 'string', description: '検索パターン(正規表現)', }, fileExtensions: { type: 'array', items: { type: 'string' }, description: '検索対象のファイル拡張子(例:[".js", ".ts"])', } }, required: ['directory', 'pattern'], }, }, { name: 'compare_files', description: '2つのファイルを比較します', inputSchema: { type: 'object', properties: { file1: { type: 'string', description: '比較対象ファイル1のパス', }, file2: { type: 'string', description: '比較対象ファイル2のパス', } }, required: ['file1', 'file2'], }, } ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { // Hello World if (request.params.name === 'hello_world') { const name = request.params.arguments.name || 'World'; return { content: [{ type: 'text', text: `こんにちは、${name}さん!` }], }; } // execute_shell_command if (request.params.name === 'execute_shell_command') { const command = request.params.arguments.command; const workingDirectory = request.params.arguments.workingDirectory || process.cwd(); // コマンドの検証 if (typeof command !== 'string' || command.trim() === '') { throw new Error('無効なコマンドです'); } // 危険なコマンドをブロック const dangerousPatterns = [ /rm\s+(-rf?|\/s)/i, // rm -rf などの危険なパターン /format\s+([a-z]:)/i, // フォーマットコマンド /del\s+[\/\\]\*\.[a-z]/i, // 広範囲の削除 ]; for (const pattern of dangerousPatterns) { if (pattern.test(command)) { throw new Error('セキュリティ上の理由により、このコマンドは実行できません'); } } // ディレクトリの存在確認 if (workingDirectory !== process.cwd() && !fs.existsSync(workingDirectory)) { throw new Error(`作業ディレクトリ "${workingDirectory}" が存在しません`); } const result = this.executeCommand(command, workingDirectory); if (result.success) { return { content: [ { type: 'text', text: `コマンド実行結果:\n${result.output || '(出力なし)'}`, }, ], }; } else { return { content: [ { type: 'text', text: `コマンド実行エラー:\n${result.error}\n\n${result.stderr || ''}`, }, ], isError: true, }; } } // search_files if (request.params.name === 'search_files') { const directory = request.params.arguments.directory; const pattern = request.params.arguments.pattern; const fileExtensions = request.params.arguments.fileExtensions || []; // パスの存在確認 if (!fs.existsSync(directory)) { throw new Error(`指定されたディレクトリ "${directory}" が存在しません`); } // パターンのバリデーション if (typeof pattern !== 'string' || pattern.trim() === '') { throw new Error('検索パターンが指定されていません'); } try { // パターンが有効な正規表現かテスト new RegExp(pattern); } catch (e) { throw new Error(`無効な正規表現パターンです: ${e.message}`); } // 検索対象ファイルの一覧を取得 const files = await this.listFilesInDirectory(directory, true); const targetFiles = files.filter(file => { if (file.isDirectory) return false; if (fileExtensions.length > 0) { const ext = path.extname(file.path).toLowerCase(); return fileExtensions.includes(ext); } return this.isTextFile(file.path); }); // 各ファイルに対して検索を実行 const searchResults = []; for (const file of targetFiles) { const results = await this.searchFileContent(file.path, pattern); searchResults.push(...results); } // 結果のフォーマット let resultText = `"${pattern}" の検索結果:\n`; resultText += `===================================\n\n`; if (searchResults.length === 0) { resultText += "一致する内容は見つかりませんでした。"; } else { resultText += `${searchResults.length} 件のマッチが見つかりました:\n\n`; searchResults.forEach((result, index) => { if (result.error) { resultText += `[エラー] ${result.file}: ${result.error}\n\n`; return; } resultText += `${index + 1}. ${result.file}:${result.line}\n`; // コンテキスト行を表示 if (result.context.before.length > 0) { resultText += `${result.context.before.map(line => ` | ${line}`).join('\n')}\n`; } // マッチした行を強調表示 resultText += ` > | ${result.content}\n`; // 後続の行を表示 if (result.context.after.length > 0) { resultText += `${result.context.after.map(line => ` | ${line}`).join('\n')}\n`; } resultText += '\n'; }); } return { content: [ { type: 'text', text: resultText, }, ], }; } // compare_files if (request.params.name === 'compare_files') { const file1Path = request.params.arguments.file1; const file2Path = request.params.arguments.file2; try { const compareResult = await this.compareFiles(file1Path, file2Path); let resultText = `ファイル比較結果:\n`; resultText += `===================================\n\n`; resultText += `ファイル1: ${file1Path}\n`; resultText += `ファイル2: ${file2Path}\n\n`; if (compareResult.identical) { resultText += compareResult.summary; } else { resultText += `${compareResult.summary}\n\n`; resultText += "差分の詳細:\n"; const maxDiffsToShow = 50; const diffsToShow = compareResult.differences.slice(0, maxDiffsToShow); diffsToShow.forEach(diff => { resultText += `行 ${diff.lineNumber}:\n`; resultText += `- ファイル1: ${diff.file1Line === null ? '(行なし)' : diff.file1Line}\n`; resultText += `+ ファイル2: ${diff.file2Line === null ? '(行なし)' : diff.file2Line}\n\n`; }); if (compareResult.differences.length > maxDiffsToShow) { resultText += `... さらに ${compareResult.differences.length - maxDiffsToShow} 行の差分があります。`; } } return { content: [ { type: 'text', text: resultText, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `ファイル比較エラー: ${error.message}`, }, ], isError: true, }; } } throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } catch (error) { console.error('Tool execution error:', error); throw new McpError( error.code || ErrorCode.InternalError, error.message || 'An unknown error occurred' ); } }); } async run() { try { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('Simple File Commander server is running on stdio transport'); } catch (error) { console.error('Error starting server:', error); process.exit(1); } } } // サーバーの起動 const server = new SimpleFileCommanderServer(); server.run().catch(console.error);

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/mokemoke0821/claude-mcp-integration'

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