Skip to main content
Glama

Claude TypeScript MCP Servers

by ukkz
git.ts22.8 kB
/** * 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; } // 軽量タグを作成 async createTag(tagName: string, target?: string): Promise<string> { try { const targetRef = target || "HEAD"; await execAsync(`git tag "${tagName.replace(/"/g, '\\"')}" ${targetRef}`, { cwd: this.repoPath, }); return `Created tag '${tagName}' at ${targetRef}`; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.includes("already exists")) { throw new Error(`Tag '${tagName}' already exists`); } throw error; } } // 注釈付きタグを作成 async createAnnotatedTag(tagName: string, message: string, target?: string): Promise<string> { try { const targetRef = target || "HEAD"; // メッセージ内のダブルクォートをエスケープ const escapedMessage = message.replace(/"/g, '\\"'); await execAsync( `git tag -a "${tagName.replace(/"/g, '\\"')}" -m "${escapedMessage}" ${targetRef}`, { cwd: this.repoPath }, ); return `Created annotated tag '${tagName}' at ${targetRef} with message: ${message}`; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.includes("already exists")) { throw new Error(`Tag '${tagName}' already exists`); } throw error; } } // タグの一覧を取得 async listTags(pattern?: string): Promise<string[]> { try { const patternArg = pattern ? `"${pattern.replace(/"/g, '\\"')}"` : ""; const { stdout } = await execAsync(`git tag -l ${patternArg}`, { cwd: this.repoPath, }); return stdout .split("\n") .filter((tag) => tag.trim() !== "") .map((tag) => tag.trim()); } catch (error) { return []; } } // タグを削除 async deleteTag(tagName: string): Promise<string> { try { await execAsync(`git tag -d "${tagName.replace(/"/g, '\\"')}"`, { cwd: this.repoPath, }); return `Deleted tag '${tagName}'`; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.includes("not found")) { throw new Error(`Tag '${tagName}' not found`); } throw error; } } // タグの詳細を表示(注釈付きタグの場合はメッセージも表示) async showTag(tagName: string): Promise<string> { try { // タグの情報を取得 const { stdout: tagInfo } = await execAsync( `git show "${tagName.replace(/"/g, '\\"')}" --format="Tag: %H%nTagger: %an <%ae>%nDate: %ad%nMessage: %s%n%b"`, { cwd: this.repoPath }, ); return tagInfo; } catch (error) { // 軽量タグの場合は、タグが指すコミットの情報を表示 try { const { stdout: commitInfo } = await execAsync( `git rev-list -n 1 "${tagName.replace(/"/g, '\\"')}"`, { cwd: this.repoPath }, ); const commitHash = commitInfo.trim(); return `Lightweight tag '${tagName}' points to commit ${commitHash}`; } catch (innerError) { throw new Error(`Tag '${tagName}' not found`); } } } } // ツール入力用の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(), }); const GitCreateTagSchema = z.object({ repo_path: z.string(), tag_name: z.string(), target: z.string().optional(), }); const GitCreateAnnotatedTagSchema = z.object({ repo_path: z.string(), tag_name: z.string(), message: z.string(), target: z.string().optional(), }); const GitListTagsSchema = z.object({ repo_path: z.string(), pattern: z.string().optional(), }); const GitDeleteTagSchema = z.object({ repo_path: z.string(), tag_name: z.string(), }); const GitShowTagSchema = z.object({ repo_path: z.string(), tag_name: 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", CREATE_TAG: "git_create_tag", CREATE_ANNOTATED_TAG: "git_create_annotated_tag", LIST_TAGS: "git_list_tags", DELETE_TAG: "git_delete_tag", SHOW_TAG: "git_show_tag", } 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, }; } }); server.tool( GitTools.CREATE_TAG, "Create a lightweight tag at the specified target (defaults to HEAD)", GitCreateTagSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.createTag(args.tag_name, args.target); 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.CREATE_ANNOTATED_TAG, "Create an annotated tag with a message at the specified target (defaults to HEAD)", GitCreateAnnotatedTagSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.createAnnotatedTag(args.tag_name, args.message, args.target); 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.LIST_TAGS, "List all tags, optionally filtered by pattern", GitListTagsSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const tags = await repo.listTags(args.pattern); const tagList = tags.length > 0 ? tags.join("\n") : "No tags found"; return { content: [ { type: "text", text: `Tags:\n${tagList}`, }, ], isError: false, }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); server.tool(GitTools.DELETE_TAG, "Delete a tag", GitDeleteTagSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.deleteTag(args.tag_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_TAG, "Show tag details (including message for annotated tags)", GitShowTagSchema.shape, async (args) => { try { const repo = new GitRepo(args.repo_path); const result = await repo.showTag(args.tag_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, }; } }, ); // サーバーを起動 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); });

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/ukkz/claude-ts-mcps'

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