Claude TypeScript MCP Servers

by ukkz
MIT License
3
  • Apple
  • src
/** * Git操作を可能にするModel Context Protocol(MCP)サーバーの実装 * このサーバーは、GitリポジトリのAPI機能を提供します */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod" import { exec } from "child_process" import { promisify } from "util" import { parseArgs } from "node:util" // コマンドライン引数の解析 const { values } = parseArgs({ options: { repository: { type: "string", short: "r", help: "Git repository path" }, verbose: { type: "boolean", short: "v", count: true, default: false, help: "Enable verbose logging" } }, allowPositionals: true, }); const repository = values.repository; const verbose = values.verbose; // 詳細度フラグに基づいてログレベルを設定 const logLevel = verbose ? "debug" : "info"; function log(level: string, ...args: any[]) { if (level === "debug" && logLevel !== "debug") return; console.error(`[${level.toUpperCase()}]`, ...args); } // Gitコマンド実行のためのexec関数のPromise化 const execAsync = promisify(exec); // Gitコマンドラッパー class GitRepo { private repoPath: string; constructor(repoPath: string) { this.repoPath = repoPath; } // 有効なGitリポジトリかどうかをチェックするシンプルなメソッド static async isValidRepo(repoPath: string): Promise<boolean> { try { await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath }); return true; } catch (error) { return false; } } // 新しいGitリポジトリを初期化 static async init(repoPath: string): Promise<string> { try { const { stdout } = await execAsync(`git init`, { cwd: repoPath }); return stdout.trim(); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); return `Error initializing repository: ${errorMsg}`; } } // リポジトリの状態を取得 async status(): Promise<string> { const { stdout } = await execAsync("git status", { cwd: this.repoPath }); return stdout; } // ステージングされていない変更を表示 async diffUnstaged(): Promise<string> { const { stdout } = await execAsync("git diff", { cwd: this.repoPath }); return stdout; } // ステージングされた変更を表示 async diffStaged(): Promise<string> { const { stdout } = await execAsync("git diff --cached", { cwd: this.repoPath }); return stdout; } // 特定のターゲットとの差分を表示 async diff(target: string): Promise<string> { const { stdout } = await execAsync(`git diff ${target}`, { cwd: this.repoPath }); return stdout; } // 変更をコミット async commit(message: string): Promise<string> { const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: this.repoPath }); // 出力からコミットハッシュを抽出 const commitHashMatch = stdout.match(/\[([a-f0-9]{7,40})\]/); const commitHash = commitHashMatch ? commitHashMatch[1] : "unknown"; return `Changes committed successfully with hash ${commitHash}`; } // ファイルをステージングエリアに追加 async add(files: string[]): Promise<string> { const fileList = files.map(file => `"${file.replace(/"/g, '\\"')}"`).join(" "); await execAsync(`git add ${fileList}`, { cwd: this.repoPath }); return "Files staged successfully"; } // ステージングされた変更をリセット async reset(): Promise<string> { await execAsync("git reset", { cwd: this.repoPath }); return "All staged changes reset"; } // コミットログを表示 async log(maxCount: number = 10): Promise<string[]> { const { stdout } = await execAsync( `git log -n ${maxCount} --pretty=format:"Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s%n"`, { cwd: this.repoPath } ); return stdout.split("\n\n").filter(entry => entry.trim() !== ""); } // 新しいブランチを作成 async createBranch(branchName: string, baseBranch?: string): Promise<string> { if (baseBranch) { await execAsync(`git branch ${branchName} ${baseBranch}`, { cwd: this.repoPath }); return `Created branch '${branchName}' from '${baseBranch}'`; } else { const { stdout: currentBranch } = await execAsync("git branch --show-current", { cwd: this.repoPath }); await execAsync(`git branch ${branchName}`, { cwd: this.repoPath }); return `Created branch '${branchName}' from '${currentBranch.trim()}'`; } } // ブランチをチェックアウト async checkout(branchName: string): Promise<string> { await execAsync(`git checkout ${branchName}`, { cwd: this.repoPath }); return `Switched to branch '${branchName}'`; } // コミットの詳細を表示 async show(revision: string): Promise<string> { // コミット詳細を取得 const { stdout: commitDetails } = await execAsync( `git show ${revision} --pretty=format:"Commit: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s%n"`, { cwd: this.repoPath } ); // 差分を取得 const { stdout: diff } = await execAsync(`git show ${revision} --format=""`, { cwd: this.repoPath }); return commitDetails + "\n" + diff; } } // ツール入力用のZodスキーマを定義 const GitStatusSchema = z.object({ repo_path: z.string() }); const GitDiffUnstagedSchema = z.object({ repo_path: z.string() }); const GitDiffStagedSchema = z.object({ repo_path: z.string() }); const GitDiffSchema = z.object({ repo_path: z.string(), target: z.string() }); const GitCommitSchema = z.object({ repo_path: z.string(), message: z.string() }); const GitAddSchema = z.object({ repo_path: z.string(), files: z.array(z.string()) }); const GitResetSchema = z.object({ repo_path: z.string() }); const GitLogSchema = z.object({ repo_path: z.string(), max_count: z.number().optional().default(10) }); const GitCreateBranchSchema = z.object({ repo_path: z.string(), branch_name: z.string(), base_branch: z.string().optional() }); const GitCheckoutSchema = z.object({ repo_path: z.string(), branch_name: z.string() }); const GitShowSchema = z.object({ repo_path: z.string(), revision: z.string() }); const GitInitSchema = z.object({ repo_path: z.string() }); // Gitツール名をenumオブジェクトとして定義 const GitTools = { STATUS: "git_status", DIFF_UNSTAGED: "git_diff_unstaged", DIFF_STAGED: "git_diff_staged", DIFF: "git_diff", COMMIT: "git_commit", ADD: "git_add", RESET: "git_reset", LOG: "git_log", CREATE_BRANCH: "git_create_branch", CHECKOUT: "git_checkout", SHOW: "git_show", INIT: "git_init" } as const; // MCPサーバーを初期化 const server = new McpServer({ name: "mcp-git", version: "1.0.0" }); // リポジトリパスが提供されている場合、有効かどうかを確認 if (repository) { GitRepo.isValidRepo(repository) .then(isValid => { if (isValid) { log("info", `Using repository at ${repository}`); } else { log("error", `${repository} is not a valid Git repository`); process.exit(1); } }) .catch(error => { log("error", `Error accessing repository: ${error}`); process.exit(1); }); } // Gitツールを定義 server.tool( GitTools.STATUS, "Shows the working tree status", GitStatusSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const status = await repo.status(); return { content: [{ type: "text", text: `Repository status:\n${status}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.DIFF_UNSTAGED, "Shows changes in the working directory that are not yet staged", GitDiffUnstagedSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const diff = await repo.diffUnstaged(); return { content: [{ type: "text", text: `Unstaged changes:\n${diff}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.DIFF_STAGED, "Shows changes that are staged for commit", GitDiffStagedSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const diff = await repo.diffStaged(); return { content: [{ type: "text", text: `Staged changes:\n${diff}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.DIFF, "Shows differences between branches or commits", GitDiffSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const diff = await repo.diff(args.target); return { content: [{ type: "text", text: `Diff with ${args.target}:\n${diff}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.COMMIT, "Records changes to the repository", GitCommitSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.commit(args.message); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.ADD, "Adds file contents to the staging area", GitAddSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.add(args.files); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.RESET, "Unstages all staged changes", GitResetSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.reset(); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.LOG, "Shows the commit logs", GitLogSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const log = await repo.log(args.max_count); return { content: [{ type: "text", text: `Commit history:\n${log.join("\n\n")}` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.CREATE_BRANCH, "Creates a new branch from an optional base branch", GitCreateBranchSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.createBranch(args.branch_name, args.base_branch); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.CHECKOUT, "Switches branches", GitCheckoutSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.checkout(args.branch_name); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.SHOW, "Shows the contents of a commit", GitShowSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.show(args.revision); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); server.tool( GitTools.INIT, "Initialize a new Git repository", GitInitSchema.shape, async (args) => { try { const result = await GitRepo.init(args.repo_path); return { content: [{ type: "text", text: result }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // サーバーを起動 async function main() { try { const transport = new StdioServerTransport(); await server.connect(transport); log("info", "Git MCP Server started"); } catch (error) { log("error", `Server error: ${error}`); process.exit(1); } } main().catch(error => { console.error("Fatal error:", error); process.exit(1); });