Skip to main content
Glama

Claude MCP Server Integration

by mokemoke0821
file-system.ts13.2 kB
import fs from 'fs-extra'; import path from 'path'; import mime from 'mime-types'; import dayjs from 'dayjs'; import { glob } from 'glob'; import { FileInfo, DirectoryInfo, DirectoryTree } from '../types/index.js'; import crypto from 'crypto'; /** * ファイルまたはディレクトリの詳細情報を取得 */ export async function getFileInfo(filePath: string): Promise<FileInfo | DirectoryInfo> { try { const stats = await fs.stat(filePath); const isDirectory = stats.isDirectory(); const name = path.basename(filePath); const extension = isDirectory ? '' : path.extname(filePath).toLowerCase(); const baseInfo = { name, path: filePath, isDirectory, size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, permissions: stats.mode.toString(8).slice(-3), isHidden: name.startsWith('.'), isSymlink: stats.isSymbolicLink(), }; if (isDirectory) { let items; try { items = await fs.readdir(filePath); } catch (error) { items = []; } let totalSize = 0; // 最大100アイテムまで計算して、それ以上の場合は概算 const sampleItems = items.slice(0, 100); for (const item of sampleItems) { try { const itemStats = await fs.stat(path.join(filePath, item)); totalSize += itemStats.size; } catch (error) { // エラーは無視 } } // サンプルから全体を推定 if (items.length > sampleItems.length) { totalSize = Math.round(totalSize * (items.length / sampleItems.length)); } return { ...baseInfo, itemCount: items.length, totalSize, isEmpty: items.length === 0, } as DirectoryInfo; } else { return { ...baseInfo, extension, mimeType: mime.lookup(filePath) || 'application/octet-stream', isExecutable: Boolean(stats.mode & 0o111), // Executableビットがあるか確認 } as FileInfo; } } catch (error) { throw new Error(`ファイル情報の取得に失敗しました: ${(error as Error).message}`); } } /** * ディレクトリ内のファイルとフォルダを再帰的にリスト */ export async function listDirectoryContents( dirPath: string, recursive = false, includeHidden = false, maxDepth = Infinity, currentDepth = 0 ): Promise<Array<FileInfo | DirectoryInfo>> { try { const entries = await fs.readdir(dirPath); let results: Array<FileInfo | DirectoryInfo> = []; for (const entry of entries) { const fullPath = path.join(dirPath, entry); // 隠しファイルをスキップ if (!includeHidden && entry.startsWith('.')) { continue; } try { const info = await getFileInfo(fullPath); results.push(info); // 再帰的に処理(最大深度まで) if (recursive && info.isDirectory && currentDepth < maxDepth) { const subEntries = await listDirectoryContents( fullPath, recursive, includeHidden, maxDepth, currentDepth + 1 ); results = results.concat(subEntries); } } catch (error) { console.error(`項目をスキップしました ${fullPath}: ${(error as Error).message}`); } } return results; } catch (error) { throw new Error(`ディレクトリの内容の一覧取得に失敗しました: ${(error as Error).message}`); } } /** * ディレクトリツリーを構築 */ export async function buildDirectoryTree( dirPath: string, options: { maxDepth?: number; includeHidden?: boolean; showFiles?: boolean; } = {} ): Promise<DirectoryTree> { const { maxDepth = Infinity, includeHidden = false, showFiles = true, } = options; try { const stats = await fs.stat(dirPath); const name = path.basename(dirPath); if (!stats.isDirectory()) { return { name, path: dirPath, type: 'file', size: stats.size, }; } const entries = await fs.readdir(dirPath); let children: DirectoryTree[] = []; if (maxDepth > 0) { for (const entry of entries) { if (!includeHidden && entry.startsWith('.')) { continue; } const fullPath = path.join(dirPath, entry); let stats; try { stats = await fs.stat(fullPath); } catch (error) { continue; // 読み取れないファイルはスキップ } if (stats.isDirectory()) { // サブディレクトリのツリーを構築 const subTree = await buildDirectoryTree(fullPath, { maxDepth: maxDepth - 1, includeHidden, showFiles, }); children.push(subTree); } else if (showFiles) { // ファイルを追加 children.push({ name: entry, path: fullPath, type: 'file', size: stats.size, }); } } } return { name, path: dirPath, type: 'directory', children: children.sort((a, b) => { // ディレクトリを先、ファイルを後にソート if (a.type === 'directory' && b.type === 'file') return -1; if (a.type === 'file' && b.type === 'directory') return 1; return a.name.localeCompare(b.name); }), }; } catch (error) { throw new Error(`ディレクトリツリーの構築に失敗しました: ${(error as Error).message}`); } } /** * パターンに一致するファイルを検索 */ export async function findFiles( rootDir: string, pattern: string, options: { recursive?: boolean; includeHidden?: boolean; includeDirectories?: boolean; includeFiles?: boolean; } = {} ): Promise<string[]> { const { recursive = true, includeHidden = false, includeDirectories = true, includeFiles = true, } = options; try { // globパターンを構築 let globPattern = path.join(rootDir, pattern); if (recursive) { globPattern = path.join(rootDir, '**', pattern); } // glob検索オプション const globOptions = { nodir: !includeDirectories, dot: includeHidden, }; // 検索実行 let matches = await glob(globPattern, globOptions); if (!includeFiles) { // ディレクトリのみにフィルタリング matches = await Promise.all( matches.map(async (match) => ({ path: match, isDirectory: (await fs.stat(match)).isDirectory(), })) ).then((results) => results.filter((result) => result.isDirectory).map((result) => result.path) ); } return matches; } catch (error) { throw new Error(`ファイルの検索に失敗しました: ${(error as Error).message}`); } } /** * ファイルまたはディレクトリをコピー */ export async function copyFileOrDirectory( sourcePath: string, destPath: string, options: { overwrite?: boolean; preserveTimestamps?: boolean; errorOnExist?: boolean; } = {} ): Promise<void> { try { await fs.copy(sourcePath, destPath, options); } catch (error) { throw new Error(`コピーに失敗しました: ${(error as Error).message}`); } } /** * ファイルまたはディレクトリを移動 */ export async function moveFileOrDirectory( sourcePath: string, destPath: string, options: { overwrite?: boolean; } = {} ): Promise<void> { try { await fs.move(sourcePath, destPath, options); } catch (error) { throw new Error(`移動に失敗しました: ${(error as Error).message}`); } } /** * 一括リネーム操作を実行 */ export async function batchRename( sourceDir: string, pattern: string, renamePattern: { search: string | RegExp; replace: string | ((match: string, ...args: any[]) => string); }, options: { recursive?: boolean; includeHidden?: boolean; dryRun?: boolean; } = {} ): Promise<{ old: string; new: string; status: 'success' | 'error' | 'skipped' | 'dry-run'; error?: string }[]> { const { recursive = false, includeHidden = false, dryRun = false, } = options; try { const files = await findFiles(sourceDir, pattern, { recursive, includeHidden, includeFiles: true, includeDirectories: false, }); const results = []; for (const filePath of files) { const dirName = path.dirname(filePath); const fileName = path.basename(filePath); // 新しいファイル名を生成 let newFileName: string; if (renamePattern.search instanceof RegExp) { newFileName = fileName.replace(renamePattern.search, renamePattern.replace as string); } else { newFileName = fileName.replace( new RegExp(renamePattern.search, 'g'), renamePattern.replace as string ); } if (fileName === newFileName) { results.push({ old: filePath, new: filePath, status: 'skipped', }); continue; } const newFilePath = path.join(dirName, newFileName); // ドライランの場合は実際のリネームを行わない if (dryRun) { results.push({ old: filePath, new: newFilePath, status: 'dry-run', }); continue; } try { // ファイルをリネーム await fs.rename(filePath, newFilePath); results.push({ old: filePath, new: newFilePath, status: 'success', }); } catch (error) { results.push({ old: filePath, new: newFilePath, status: 'error', error: (error as Error).message, }); } } return results; } catch (error) { throw new Error(`一括リネームに失敗しました: ${(error as Error).message}`); } } /** * ファイルのハッシュを計算 */ export async function calculateFileHash( filePath: string, algorithm = 'md5' ): Promise<string> { return new Promise((resolve, reject) => { try { const hash = crypto.createHash(algorithm); const stream = fs.createReadStream(filePath); stream.on('data', (data) => { hash.update(data); }); stream.on('end', () => { resolve(hash.digest('hex')); }); stream.on('error', (error) => { reject(new Error(`ハッシュ計算中にエラーが発生しました: ${error.message}`)); }); } catch (error) { reject(new Error(`ハッシュ計算の初期化に失敗しました: ${(error as Error).message}`)); } }); } /** * ファイルの内容を検索 */ export async function searchFileContents( filePath: string, searchPattern: string | RegExp, options: { maxResults?: number; contextLines?: number; caseSensitive?: boolean; } = {} ): Promise<{ line: number; content: string; context: string[]; }[]> { const { maxResults = 100, contextLines = 2, caseSensitive = false, } = options; try { // ファイルの内容を読み込む const content = await fs.readFile(filePath, 'utf8'); const lines = content.split(/\r?\n/); const results = []; // 正規表現を準備 const regex = searchPattern instanceof RegExp ? searchPattern : new RegExp(searchPattern, caseSensitive ? 'g' : 'gi'); // 行ごとに検索 for (let i = 0; i < lines.length && results.length < maxResults; i++) { const line = lines[i]; if (regex.test(line)) { // マッチした行の前後のコンテキスト行を取得 const contextStart = Math.max(0, i - contextLines); const contextEnd = Math.min(lines.length - 1, i + contextLines); const context = []; for (let j = contextStart; j <= contextEnd; j++) { if (j !== i) { context.push(`${j + 1}: ${lines[j]}`); } } results.push({ line: i + 1, content: line, context, }); } } return results; } catch (error) { throw new Error(`ファイル内容の検索に失敗しました: ${(error as Error).message}`); } } /** * ヒューマンリーダブルなサイズに変換 */ export function formatFileSize(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; } /** * ファイル日時の表示フォーマットを整形 */ export function formatFileDate(date: Date, format = 'YYYY-MM-DD HH:mm:ss'): string { return dayjs(date).format(format); }

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