Claude TypeScript MCP Servers
by ukkz
- 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);
});