From 62a9d01035a57231e63f52fc063cab0a12b1117a Mon Sep 17 00:00:00 2001 From: "ethan.chen" Date: Thu, 8 Jan 2026 11:01:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20git=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 + src/index.ts | 2 + src/tools/programming/git.ts | 672 +++++++++++++++++++++++++++++++++++ 3 files changed, 682 insertions(+) create mode 100644 src/tools/programming/git.ts diff --git a/README.md b/README.md index 0b76627..464cf5f 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,14 @@ Example configuration for Claude Desktop (`claude_desktop_config.json`): - `docs_bun` - Get Bun documentation - `code_review` - Review code - `code_optimize` - Get optimization suggestions +- `git_status` - Get git repository status +- `git_add` - Stage files for commit +- `git_commit` - Commit staged changes +- `git_push` - Push commits to remote +- `git_pull` - Pull latest changes from remote +- `git_log` - Show commit history +- `git_branch` - List, create, or delete branches +- `git_diff` - Show changes between commits or working directory ### DevOps - `nas_list_files` - List NAS files diff --git a/src/index.ts b/src/index.ts index 03463bf..0fd2c9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { registerCodeSnippetTools } from "./tools/programming/codeSnippet.js"; import { registerProjectTemplateTools } from "./tools/programming/projectTemplate.js"; import { registerDocsTools } from "./tools/programming/docs.js"; import { registerCodeReviewTools } from "./tools/programming/codeReview.js"; +import { registerGitTools } from "./tools/programming/git.js"; import { registerNASTools } from "./tools/devops/nas.js"; import { registerServerTools } from "./tools/devops/server.js"; @@ -35,6 +36,7 @@ registerCodeSnippetTools(); registerProjectTemplateTools(); registerDocsTools(); registerCodeReviewTools(); +registerGitTools(); // DevOps tools registerNASTools(); diff --git a/src/tools/programming/git.ts b/src/tools/programming/git.ts new file mode 100644 index 0000000..9a7e345 --- /dev/null +++ b/src/tools/programming/git.ts @@ -0,0 +1,672 @@ +/** + * Git version control tools + */ + +import { mcpServer } from "../../server.js"; +import { logger } from "../../utils/logger.js"; +import { execSync } from "child_process"; +import { existsSync } from "fs"; +import { join } from "path"; + +export function registerGitTools(): void { + // Git status + mcpServer.registerTool( + { + name: "git_status", + description: "Get git repository status (working directory, staged files, branch)", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const gitDir = join(repoPath, ".git"); + + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + const status = execSync("git status --short", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + const branch = execSync("git rev-parse --abbrev-ref HEAD", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + const lastCommit = execSync("git log -1 --oneline", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + let output = `Git Status (${repoPath})\n\n`; + output += `Branch: ${branch}\n`; + output += `Last commit: ${lastCommit}\n\n`; + + if (status) { + output += `Working directory changes:\n${status}`; + } else { + output += "Working directory clean"; + } + + return { + content: [ + { + type: "text", + text: output, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); + + // Git add + mcpServer.registerTool( + { + name: "git_add", + description: "Stage files for commit", + inputSchema: { + type: "object", + properties: { + files: { + type: "array", + items: { type: "string" }, + description: "Files to stage (use '.' for all files)", + }, + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + required: ["files"], + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const files = args.files as string[]; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + const filesToAdd = files.length === 0 ? ["."] : files; + execSync(`git add ${filesToAdd.join(" ")}`, { + cwd: repoPath, + encoding: "utf-8", + }); + + const staged = execSync("git diff --cached --name-only", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + return { + content: [ + { + type: "text", + text: `Files staged successfully!\n\nStaged files:\n${staged || "No files staged"}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); + + // Git commit + mcpServer.registerTool( + { + name: "git_commit", + description: "Commit staged changes", + inputSchema: { + type: "object", + properties: { + message: { + type: "string", + description: "Commit message", + }, + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + required: ["message"], + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const message = args.message as string; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + execSync(`git commit -m "${message}"`, { + cwd: repoPath, + encoding: "utf-8", + }); + + const commitHash = execSync("git rev-parse HEAD", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + const commitInfo = execSync("git log -1 --oneline", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + return { + content: [ + { + type: "text", + text: `Commit created successfully!\n\n${commitInfo}\nHash: ${commitHash}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}\n\nNote: Make sure you have staged files before committing.`, + }, + ], + isError: true, + }; + } + } + ); + + // Git push + mcpServer.registerTool( + { + name: "git_push", + description: "Push commits to remote repository", + inputSchema: { + type: "object", + properties: { + remote: { + type: "string", + description: "Remote name (default: origin)", + default: "origin", + }, + branch: { + type: "string", + description: "Branch to push (default: current branch)", + }, + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const remote = (args.remote as string) || "origin"; + const branch = args.branch as string | undefined; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + const currentBranch = + branch || + execSync("git rev-parse --abbrev-ref HEAD", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + execSync(`git push ${remote} ${currentBranch}`, { + cwd: repoPath, + encoding: "utf-8", + }); + + return { + content: [ + { + type: "text", + text: `Pushed to ${remote}/${currentBranch} successfully!`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}\n\nNote: Make sure you have commits to push and remote is configured.`, + }, + ], + isError: true, + }; + } + } + ); + + // Git pull + mcpServer.registerTool( + { + name: "git_pull", + description: "Pull latest changes from remote repository", + inputSchema: { + type: "object", + properties: { + remote: { + type: "string", + description: "Remote name (default: origin)", + default: "origin", + }, + branch: { + type: "string", + description: "Branch to pull (default: current branch)", + }, + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const remote = (args.remote as string) || "origin"; + const branch = args.branch as string | undefined; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + const currentBranch = + branch || + execSync("git rev-parse --abbrev-ref HEAD", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + const output = execSync(`git pull ${remote} ${currentBranch}`, { + cwd: repoPath, + encoding: "utf-8", + }); + + return { + content: [ + { + type: "text", + text: `Pulled from ${remote}/${currentBranch} successfully!\n\n${output}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); + + // Git log + mcpServer.registerTool( + { + name: "git_log", + description: "Show commit history", + inputSchema: { + type: "object", + properties: { + limit: { + type: "number", + description: "Number of commits to show (default: 10)", + default: 10, + }, + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const limit = (args.limit as number) || 10; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + const log = execSync(`git log -${limit} --oneline --decorate`, { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + return { + content: [ + { + type: "text", + text: `Recent commits (${limit}):\n\n${log || "No commits found"}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); + + // Git branch + mcpServer.registerTool( + { + name: "git_branch", + description: "List, create, or delete branches", + inputSchema: { + type: "object", + properties: { + action: { + type: "string", + description: "Action: 'list', 'create', or 'delete'", + enum: ["list", "create", "delete"], + default: "list", + }, + name: { + type: "string", + description: "Branch name (required for create/delete)", + }, + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + }, + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const action = (args.action as string) || "list"; + const name = args.name as string | undefined; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + if (action === "list") { + const branches = execSync("git branch -a", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + return { + content: [ + { + type: "text", + text: `Branches:\n\n${branches}\n\nCurrent branch: ${currentBranch}`, + }, + ], + }; + } else if (action === "create") { + if (!name) { + return { + content: [ + { + type: "text", + text: "Error: Branch name is required for create action", + }, + ], + isError: true, + }; + } + + execSync(`git branch ${name}`, { + cwd: repoPath, + encoding: "utf-8", + }); + + return { + content: [ + { + type: "text", + text: `Branch "${name}" created successfully!`, + }, + ], + }; + } else if (action === "delete") { + if (!name) { + return { + content: [ + { + type: "text", + text: "Error: Branch name is required for delete action", + }, + ], + isError: true, + }; + } + + execSync(`git branch -d ${name}`, { + cwd: repoPath, + encoding: "utf-8", + }); + + return { + content: [ + { + type: "text", + text: `Branch "${name}" deleted successfully!`, + }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: "Invalid action", + }, + ], + isError: true, + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); + + // Git diff + mcpServer.registerTool( + { + name: "git_diff", + description: "Show changes between commits, branches, or working directory", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Path to git repository (default: current directory)", + default: ".", + }, + file: { + type: "string", + description: "Specific file to show diff (optional)", + }, + }, + }, + }, + async (args) => { + try { + const repoPath = (args.path as string) || process.cwd(); + const file = args.file as string | undefined; + + const gitDir = join(repoPath, ".git"); + if (!existsSync(gitDir)) { + return { + content: [ + { + type: "text", + text: `Error: Not a git repository: ${repoPath}`, + }, + ], + isError: true, + }; + } + + const diffCommand = file ? `git diff ${file}` : "git diff"; + const diff = execSync(diffCommand, { + cwd: repoPath, + encoding: "utf-8", + }).trim(); + + return { + content: [ + { + type: "text", + text: diff || "No changes found", + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + } + ); +} + + +