@cyanheads/git-mcp-server 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Branch Tools
3
+ * ===========
4
+ *
5
+ * MCP tools for Git branch operations.
6
+ */
7
+ import { z } from 'zod';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ /**
11
+ * Registers branch tools with the MCP server
12
+ *
13
+ * @param server - MCP server instance
14
+ */
15
+ export function setupBranchTools(server) {
16
+ // List branches
17
+ server.tool("git_branch_list", "List branches in a repository. Displays both local and optionally remote branches, clearly marking the current branch.", {
18
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
19
+ all: z.boolean().optional().default(false).describe("Whether to include remote branches in the list")
20
+ }, async ({ path, all }) => {
21
+ try {
22
+ const normalizedPath = PathValidation.normalizePath(path);
23
+ const gitService = new GitService(normalizedPath);
24
+ // Check if this is a git repository
25
+ const isRepo = await gitService.isGitRepository();
26
+ if (!isRepo) {
27
+ return {
28
+ content: [{
29
+ type: "text",
30
+ text: `Error: Not a Git repository: ${normalizedPath}`
31
+ }],
32
+ isError: true
33
+ };
34
+ }
35
+ const result = await gitService.listBranches(all);
36
+ if (!result.resultSuccessful) {
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: `Error: ${result.resultError.errorMessage}`
41
+ }],
42
+ isError: true
43
+ };
44
+ }
45
+ // The listBranches result now contains the current branch
46
+ const branchSummary = result.resultData;
47
+ const currentBranch = branchSummary.current;
48
+ const allBranches = branchSummary.all; // Get all branch names from the summary
49
+ if (allBranches.length === 0) { // Check the length of the derived array
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: `No branches found in repository at: ${normalizedPath}`
54
+ }]
55
+ };
56
+ }
57
+ // Format output
58
+ let output = `Branches in repository at: ${normalizedPath}\n\n`;
59
+ allBranches.forEach(branch => {
60
+ // Clean up potential remote prefixes like 'remotes/origin/' for display if needed
61
+ const displayBranch = branch.replace(/^remotes\/[^\/]+\//, '');
62
+ if (displayBranch === currentBranch) {
63
+ output += `* ${displayBranch} (current)\n`;
64
+ }
65
+ else {
66
+ output += ` ${displayBranch}\n`;
67
+ }
68
+ });
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: output
73
+ }]
74
+ };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
81
+ }],
82
+ isError: true
83
+ };
84
+ }
85
+ });
86
+ // Create branch
87
+ server.tool("git_branch_create", "Create a new branch. Creates a new branch at the specified reference point (commit or branch) and optionally checks it out.", {
88
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
89
+ name: z.string().min(1, "Branch name is required").describe("Name of the new branch to create"),
90
+ startPoint: z.string().optional().describe("Reference (commit, branch) to create the branch from"),
91
+ checkout: z.boolean().optional().default(false).describe("Whether to checkout the newly created branch")
92
+ }, async ({ path, name, startPoint, checkout }) => {
93
+ try {
94
+ const normalizedPath = PathValidation.normalizePath(path);
95
+ const gitService = new GitService(normalizedPath);
96
+ // Check if this is a git repository
97
+ const isRepo = await gitService.isGitRepository();
98
+ if (!isRepo) {
99
+ return {
100
+ content: [{
101
+ type: "text",
102
+ text: `Error: Not a Git repository: ${normalizedPath}`
103
+ }],
104
+ isError: true
105
+ };
106
+ }
107
+ const result = await gitService.createBranch({
108
+ name,
109
+ startPoint,
110
+ checkout
111
+ });
112
+ if (!result.resultSuccessful) {
113
+ return {
114
+ content: [{
115
+ type: "text",
116
+ text: `Error: ${result.resultError.errorMessage}`
117
+ }],
118
+ isError: true
119
+ };
120
+ }
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: `Successfully created branch '${name}'${checkout ? ' and checked it out' : ''}`
125
+ }]
126
+ };
127
+ }
128
+ catch (error) {
129
+ return {
130
+ content: [{
131
+ type: "text",
132
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
133
+ }],
134
+ isError: true
135
+ };
136
+ }
137
+ });
138
+ // Checkout branch
139
+ server.tool("git_checkout", "Checkout a branch, tag, or commit. Switches the working directory to the specified target and updates HEAD to point to it. Can optionally create a new branch.", {
140
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
141
+ target: z.string().min(1, "Branch or commit to checkout is required").describe("Branch name, tag, or commit hash to checkout"),
142
+ createBranch: z.boolean().optional().default(false).describe("Whether to create a new branch with the specified name")
143
+ }, async ({ path, target, createBranch }) => {
144
+ try {
145
+ const normalizedPath = PathValidation.normalizePath(path);
146
+ const gitService = new GitService(normalizedPath);
147
+ // Check if this is a git repository
148
+ const isRepo = await gitService.isGitRepository();
149
+ if (!isRepo) {
150
+ return {
151
+ content: [{
152
+ type: "text",
153
+ text: `Error: Not a Git repository: ${normalizedPath}`
154
+ }],
155
+ isError: true
156
+ };
157
+ }
158
+ const result = await gitService.checkout(target, createBranch);
159
+ if (!result.resultSuccessful) {
160
+ return {
161
+ content: [{
162
+ type: "text",
163
+ text: `Error: ${result.resultError.errorMessage}`
164
+ }],
165
+ isError: true
166
+ };
167
+ }
168
+ return {
169
+ content: [{
170
+ type: "text",
171
+ text: `Successfully checked out '${target}'${createBranch ? ' (new branch)' : ''}`
172
+ }]
173
+ };
174
+ }
175
+ catch (error) {
176
+ return {
177
+ content: [{
178
+ type: "text",
179
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
180
+ }],
181
+ isError: true
182
+ };
183
+ }
184
+ });
185
+ // Delete branch
186
+ server.tool("git_branch_delete", "Delete a branch. Removes the specified branch from the repository. By default, only fully merged branches can be deleted unless force is set to true.", {
187
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
188
+ branch: z.string().min(1, "Branch name is required").describe("Name of the branch to delete"),
189
+ force: z.boolean().optional().default(false).describe("Force deletion even if branch is not fully merged")
190
+ }, async ({ path, branch, force }) => {
191
+ try {
192
+ const normalizedPath = PathValidation.normalizePath(path);
193
+ const gitService = new GitService(normalizedPath);
194
+ // Check if this is a git repository
195
+ const isRepo = await gitService.isGitRepository();
196
+ if (!isRepo) {
197
+ return {
198
+ content: [{
199
+ type: "text",
200
+ text: `Error: Not a Git repository: ${normalizedPath}`
201
+ }],
202
+ isError: true
203
+ };
204
+ }
205
+ const result = await gitService.deleteBranch(branch, force);
206
+ if (!result.resultSuccessful) {
207
+ return {
208
+ content: [{
209
+ type: "text",
210
+ text: `Error: ${result.resultError.errorMessage}`
211
+ }],
212
+ isError: true
213
+ };
214
+ }
215
+ return {
216
+ content: [{
217
+ type: "text",
218
+ text: `Successfully deleted branch '${branch}'`
219
+ }]
220
+ };
221
+ }
222
+ catch (error) {
223
+ return {
224
+ content: [{
225
+ type: "text",
226
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
227
+ }],
228
+ isError: true
229
+ };
230
+ }
231
+ });
232
+ // Merge branch
233
+ server.tool("git_merge", "Merge a branch into the current branch. Combines changes from the specified branch into the current branch with configurable merge strategies.", {
234
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
235
+ branch: z.string().min(1, "Branch to merge is required").describe("Name of the branch to merge into the current branch"),
236
+ message: z.string().optional().describe("Custom commit message for the merge commit"),
237
+ fastForwardOnly: z.boolean().optional().default(false).describe("Only allow fast-forward merges (fail if not possible)"),
238
+ noFastForward: z.boolean().optional().default(false).describe("Create a merge commit even when fast-forward is possible")
239
+ }, async ({ path, branch, message, fastForwardOnly, noFastForward }) => {
240
+ try {
241
+ const normalizedPath = PathValidation.normalizePath(path);
242
+ const gitService = new GitService(normalizedPath);
243
+ // Check if this is a git repository
244
+ const isRepo = await gitService.isGitRepository();
245
+ if (!isRepo) {
246
+ return {
247
+ content: [{
248
+ type: "text",
249
+ text: `Error: Not a Git repository: ${normalizedPath}`
250
+ }],
251
+ isError: true
252
+ };
253
+ }
254
+ // Can't have both fastForwardOnly and noFastForward
255
+ if (fastForwardOnly && noFastForward) {
256
+ return {
257
+ content: [{
258
+ type: "text",
259
+ text: `Error: Cannot specify both fastForwardOnly and noFastForward`
260
+ }],
261
+ isError: true
262
+ };
263
+ }
264
+ const result = await gitService.merge({
265
+ branch,
266
+ message,
267
+ fastForwardOnly,
268
+ noFastForward
269
+ });
270
+ if (!result.resultSuccessful) {
271
+ return {
272
+ content: [{
273
+ type: "text",
274
+ text: `Error: ${result.resultError.errorMessage}`
275
+ }],
276
+ isError: true
277
+ };
278
+ }
279
+ return {
280
+ content: [{
281
+ type: "text",
282
+ text: `Successfully merged branch '${branch}' into current branch`
283
+ }]
284
+ };
285
+ }
286
+ catch (error) {
287
+ return {
288
+ content: [{
289
+ type: "text",
290
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
291
+ }],
292
+ isError: true
293
+ };
294
+ }
295
+ });
296
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * MCP Tool Handlers
3
+ * ================
4
+ *
5
+ * Entry point for all MCP tool implementations.
6
+ * This module registers all tool handlers with the MCP server.
7
+ */
8
+ import { setupRepositoryTools } from './repository.js';
9
+ import { setupBranchTools } from './branch.js';
10
+ import { setupWorkdirTools } from './workdir.js';
11
+ import { setupRemoteTools } from './remote.js';
12
+ import { setupAdvancedTools } from './advanced.js';
13
+ /**
14
+ * Registers all Git MCP tools with the server
15
+ *
16
+ * @param server - MCP server instance
17
+ */
18
+ export function registerAllTools(server) {
19
+ // Repository operations (init, clone, status)
20
+ setupRepositoryTools(server);
21
+ // Branch operations (create, checkout, merge, etc.)
22
+ setupBranchTools(server);
23
+ // Working directory operations (stage, unstage, commit, etc.)
24
+ setupWorkdirTools(server);
25
+ // Remote operations (add, fetch, pull, push, etc.)
26
+ setupRemoteTools(server);
27
+ // Advanced operations (stash, cherry-pick, rebase, etc.)
28
+ setupAdvancedTools(server);
29
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Remote Tools
3
+ * ===========
4
+ *
5
+ * MCP tools for Git remote operations.
6
+ */
7
+ import { z } from 'zod';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ /**
11
+ * Registers remote operation tools with the MCP server
12
+ *
13
+ * @param server - MCP server instance
14
+ */
15
+ export function setupRemoteTools(server) {
16
+ // Add a remote
17
+ server.tool("git_remote_add", "Add a new remote repository reference. Creates a connection to a remote repository with a name and URL, allowing fetching and pushing changes to and from that repository.", {
18
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
19
+ name: z.string().min(1, "Remote name is required").describe("Name for the remote repository (e.g., 'origin')"),
20
+ url: z.string().url("Invalid URL format").describe("URL of the remote repository")
21
+ }, async ({ path, name, url }) => {
22
+ try {
23
+ const normalizedPath = PathValidation.normalizePath(path);
24
+ const gitService = new GitService(normalizedPath);
25
+ // Check if this is a git repository
26
+ const isRepo = await gitService.isGitRepository();
27
+ if (!isRepo) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: `Error: Not a Git repository: ${normalizedPath}`
32
+ }],
33
+ isError: true
34
+ };
35
+ }
36
+ const result = await gitService.addRemote({
37
+ name,
38
+ url
39
+ });
40
+ if (!result.resultSuccessful) {
41
+ return {
42
+ content: [{
43
+ type: "text",
44
+ text: `Error: ${result.resultError.errorMessage}`
45
+ }],
46
+ isError: true
47
+ };
48
+ }
49
+ return {
50
+ content: [{
51
+ type: "text",
52
+ text: `Successfully added remote '${name}' with URL '${url}'`
53
+ }]
54
+ };
55
+ }
56
+ catch (error) {
57
+ return {
58
+ content: [{
59
+ type: "text",
60
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
61
+ }],
62
+ isError: true
63
+ };
64
+ }
65
+ });
66
+ // List remotes
67
+ server.tool("git_remote_list", "List all configured remote repositories. Displays the names and URLs of all remotes associated with the repository, showing both fetch and push URLs.", {
68
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository")
69
+ }, async ({ path }) => {
70
+ try {
71
+ const normalizedPath = PathValidation.normalizePath(path);
72
+ const gitService = new GitService(normalizedPath);
73
+ // Check if this is a git repository
74
+ const isRepo = await gitService.isGitRepository();
75
+ if (!isRepo) {
76
+ return {
77
+ content: [{
78
+ type: "text",
79
+ text: `Error: Not a Git repository: ${normalizedPath}`
80
+ }],
81
+ isError: true
82
+ };
83
+ }
84
+ const result = await gitService.listRemotes();
85
+ if (!result.resultSuccessful) {
86
+ return {
87
+ content: [{
88
+ type: "text",
89
+ text: `Error: ${result.resultError.errorMessage}`
90
+ }],
91
+ isError: true
92
+ };
93
+ }
94
+ if (result.resultData.length === 0) {
95
+ return {
96
+ content: [{
97
+ type: "text",
98
+ text: `No remotes found in repository at: ${normalizedPath}`
99
+ }]
100
+ };
101
+ }
102
+ // Format output
103
+ let output = `Remotes in repository at: ${normalizedPath}\n\n`;
104
+ result.resultData.forEach(remote => {
105
+ output += `${remote.name}\n`;
106
+ output += ` fetch: ${remote.refs.fetch}\n`;
107
+ output += ` push: ${remote.refs.push}\n\n`;
108
+ });
109
+ return {
110
+ content: [{
111
+ type: "text",
112
+ text: output
113
+ }]
114
+ };
115
+ }
116
+ catch (error) {
117
+ return {
118
+ content: [{
119
+ type: "text",
120
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
121
+ }],
122
+ isError: true
123
+ };
124
+ }
125
+ });
126
+ // Fetch from remote
127
+ server.tool("git_fetch", "Fetch changes from a remote repository. Downloads objects and refs from a remote repository without merging them into local branches.", {
128
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
129
+ remote: z.string().optional().default("origin").describe("Name of the remote to fetch from (defaults to 'origin')"),
130
+ branch: z.string().optional().describe("Specific branch to fetch (fetches all branches if omitted)")
131
+ }, async ({ path, remote, branch }) => {
132
+ try {
133
+ const normalizedPath = PathValidation.normalizePath(path);
134
+ const gitService = new GitService(normalizedPath);
135
+ // Check if this is a git repository
136
+ const isRepo = await gitService.isGitRepository();
137
+ if (!isRepo) {
138
+ return {
139
+ content: [{
140
+ type: "text",
141
+ text: `Error: Not a Git repository: ${normalizedPath}`
142
+ }],
143
+ isError: true
144
+ };
145
+ }
146
+ const result = await gitService.fetch(remote, branch);
147
+ if (!result.resultSuccessful) {
148
+ return {
149
+ content: [{
150
+ type: "text",
151
+ text: `Error: ${result.resultError.errorMessage}`
152
+ }],
153
+ isError: true
154
+ };
155
+ }
156
+ return {
157
+ content: [{
158
+ type: "text",
159
+ text: `Successfully fetched from remote '${remote}'${branch ? ` branch '${branch}'` : ''}`
160
+ }]
161
+ };
162
+ }
163
+ catch (error) {
164
+ return {
165
+ content: [{
166
+ type: "text",
167
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
168
+ }],
169
+ isError: true
170
+ };
171
+ }
172
+ });
173
+ // Pull from remote
174
+ server.tool("git_pull", "Pull changes from a remote repository. Fetches from a remote repository and integrates changes into the current branch, either by merging or rebasing.", {
175
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
176
+ remote: z.string().optional().describe("Name of the remote to pull from (defaults to origin)"),
177
+ branch: z.string().optional().describe("Branch to pull from (defaults to current tracking branch)"),
178
+ rebase: z.boolean().optional().default(false).describe("Whether to use rebase instead of merge when pulling")
179
+ }, async ({ path, remote, branch, rebase }) => {
180
+ try {
181
+ const normalizedPath = PathValidation.normalizePath(path);
182
+ const gitService = new GitService(normalizedPath);
183
+ // Check if this is a git repository
184
+ const isRepo = await gitService.isGitRepository();
185
+ if (!isRepo) {
186
+ return {
187
+ content: [{
188
+ type: "text",
189
+ text: `Error: Not a Git repository: ${normalizedPath}`
190
+ }],
191
+ isError: true
192
+ };
193
+ }
194
+ const result = await gitService.pull({
195
+ remote,
196
+ branch,
197
+ rebase
198
+ });
199
+ if (!result.resultSuccessful) {
200
+ return {
201
+ content: [{
202
+ type: "text",
203
+ text: `Error: ${result.resultError.errorMessage}`
204
+ }],
205
+ isError: true
206
+ };
207
+ }
208
+ return {
209
+ content: [{
210
+ type: "text",
211
+ text: `Successfully pulled changes${remote ? ` from remote '${remote}'` : ''}${branch ? ` branch '${branch}'` : ''}${rebase ? ' with rebase' : ''}`
212
+ }]
213
+ };
214
+ }
215
+ catch (error) {
216
+ return {
217
+ content: [{
218
+ type: "text",
219
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
220
+ }],
221
+ isError: true
222
+ };
223
+ }
224
+ });
225
+ // Push to remote
226
+ server.tool("git_push", "Push local changes to a remote repository. Uploads local branch commits to the remote repository, updating remote references.", {
227
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
228
+ remote: z.string().optional().default("origin").describe("Name of the remote to push to (defaults to 'origin')"),
229
+ branch: z.string().optional().describe("Branch to push (defaults to current branch)"),
230
+ force: z.boolean().optional().default(false).describe("Force push changes, overwriting remote history"),
231
+ setUpstream: z.boolean().optional().default(false).describe("Set upstream tracking for the branch being pushed")
232
+ }, async ({ path, remote, branch, force, setUpstream }) => {
233
+ try {
234
+ const normalizedPath = PathValidation.normalizePath(path);
235
+ const gitService = new GitService(normalizedPath);
236
+ // Check if this is a git repository
237
+ const isRepo = await gitService.isGitRepository();
238
+ if (!isRepo) {
239
+ return {
240
+ content: [{
241
+ type: "text",
242
+ text: `Error: Not a Git repository: ${normalizedPath}`
243
+ }],
244
+ isError: true
245
+ };
246
+ }
247
+ const result = await gitService.push({
248
+ remote,
249
+ branch,
250
+ force,
251
+ setUpstream
252
+ });
253
+ if (!result.resultSuccessful) {
254
+ return {
255
+ content: [{
256
+ type: "text",
257
+ text: `Error: ${result.resultError.errorMessage}`
258
+ }],
259
+ isError: true
260
+ };
261
+ }
262
+ return {
263
+ content: [{
264
+ type: "text",
265
+ text: `Successfully pushed changes to remote '${remote}'${branch ? ` branch '${branch}'` : ''}${force ? ' (force push)' : ''}${setUpstream ? ' (set upstream)' : ''}`
266
+ }]
267
+ };
268
+ }
269
+ catch (error) {
270
+ return {
271
+ content: [{
272
+ type: "text",
273
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
274
+ }],
275
+ isError: true
276
+ };
277
+ }
278
+ });
279
+ }