@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,526 @@
1
+ /**
2
+ * Advanced Git Tools
3
+ * ================
4
+ *
5
+ * MCP tools for advanced Git operations like stashing, tagging, rebasing, etc.
6
+ */
7
+ import { z } from 'zod';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ /**
11
+ * Registers advanced Git tools with the MCP server
12
+ *
13
+ * @param server - MCP server instance
14
+ */
15
+ export function setupAdvancedTools(server) {
16
+ // Create tag
17
+ server.tool("git_tag_create", "Create a new tag in the repository. Tags are references that point to specific commits, useful for marking release points or important commits. Can create lightweight tags or annotated tags with messages.", {
18
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
19
+ name: z.string().min(1, "Tag name is required").describe("Name for the new tag"),
20
+ message: z.string().optional().describe("Optional message for an annotated tag"),
21
+ ref: z.string().optional().describe("Reference (commit, branch) to create the tag at")
22
+ }, async ({ path, name, message, ref }) => {
23
+ try {
24
+ const normalizedPath = PathValidation.normalizePath(path);
25
+ const gitService = new GitService(normalizedPath);
26
+ // Check if this is a git repository
27
+ const isRepo = await gitService.isGitRepository();
28
+ if (!isRepo) {
29
+ return {
30
+ content: [{
31
+ type: "text",
32
+ text: `Error: Not a Git repository: ${normalizedPath}`
33
+ }],
34
+ isError: true
35
+ };
36
+ }
37
+ const result = await gitService.createTag({
38
+ name,
39
+ message,
40
+ ref
41
+ });
42
+ if (!result.resultSuccessful) {
43
+ return {
44
+ content: [{
45
+ type: "text",
46
+ text: `Error: ${result.resultError.errorMessage}`
47
+ }],
48
+ isError: true
49
+ };
50
+ }
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: `Successfully created ${message ? 'annotated ' : ''}tag '${name}'${ref ? ` at ref '${ref}'` : ''}`
55
+ }]
56
+ };
57
+ }
58
+ catch (error) {
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
63
+ }],
64
+ isError: true
65
+ };
66
+ }
67
+ });
68
+ // List tags
69
+ server.tool("git_tag_list", "List all tags in the repository. Displays all tag names that exist in the repository, which can be used to identify releases or important reference points.", {
70
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository")
71
+ }, async ({ path }) => {
72
+ try {
73
+ const normalizedPath = PathValidation.normalizePath(path);
74
+ const gitService = new GitService(normalizedPath);
75
+ // Check if this is a git repository
76
+ const isRepo = await gitService.isGitRepository();
77
+ if (!isRepo) {
78
+ return {
79
+ content: [{
80
+ type: "text",
81
+ text: `Error: Not a Git repository: ${normalizedPath}`
82
+ }],
83
+ isError: true
84
+ };
85
+ }
86
+ const result = await gitService.listTags();
87
+ if (!result.resultSuccessful) {
88
+ return {
89
+ content: [{
90
+ type: "text",
91
+ text: `Error: ${result.resultError.errorMessage}`
92
+ }],
93
+ isError: true
94
+ };
95
+ }
96
+ if (result.resultData.length === 0) {
97
+ return {
98
+ content: [{
99
+ type: "text",
100
+ text: `No tags found in repository at: ${normalizedPath}`
101
+ }]
102
+ };
103
+ }
104
+ // Format output
105
+ let output = `Tags in repository at: ${normalizedPath}\n\n`;
106
+ result.resultData.forEach(tag => {
107
+ output += `${tag}\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
+ // Create stash
127
+ server.tool("git_stash_create", "Save uncommitted changes to a stash. Captures the current state of working directory and index and saves it on a stack of stashes, allowing you to switch branches without committing in-progress work.", {
128
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
129
+ message: z.string().optional().describe("Optional description for the stash"),
130
+ includeUntracked: z.boolean().optional().default(false).describe("Whether to include untracked files in the stash")
131
+ }, async ({ path, message, includeUntracked }) => {
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.createStash({
147
+ message,
148
+ includeUntracked
149
+ });
150
+ if (!result.resultSuccessful) {
151
+ return {
152
+ content: [{
153
+ type: "text",
154
+ text: `Error: ${result.resultError.errorMessage}`
155
+ }],
156
+ isError: true
157
+ };
158
+ }
159
+ return {
160
+ content: [{
161
+ type: "text",
162
+ text: `Successfully created stash${message ? ` with message: "${message}"` : ''}${includeUntracked ? ' (including untracked files)' : ''}`
163
+ }]
164
+ };
165
+ }
166
+ catch (error) {
167
+ return {
168
+ content: [{
169
+ type: "text",
170
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
171
+ }],
172
+ isError: true
173
+ };
174
+ }
175
+ });
176
+ // List stashes
177
+ server.tool("git_stash_list", "List all stashes in the repository. Shows the stack of stashes that have been created and their descriptions, allowing you to identify the stash you want to apply or pop.", {
178
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository")
179
+ }, async ({ path }) => {
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.listStashes();
195
+ if (!result.resultSuccessful) {
196
+ return {
197
+ content: [{
198
+ type: "text",
199
+ text: `Error: ${result.resultError.errorMessage}`
200
+ }],
201
+ isError: true
202
+ };
203
+ }
204
+ if (result.resultData.trim() === '') {
205
+ return {
206
+ content: [{
207
+ type: "text",
208
+ text: `No stashes found in repository at: ${normalizedPath}`
209
+ }]
210
+ };
211
+ }
212
+ return {
213
+ content: [{
214
+ type: "text",
215
+ text: `Stashes in repository at: ${normalizedPath}\n\n${result.resultData}`
216
+ }]
217
+ };
218
+ }
219
+ catch (error) {
220
+ return {
221
+ content: [{
222
+ type: "text",
223
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
224
+ }],
225
+ isError: true
226
+ };
227
+ }
228
+ });
229
+ // Apply stash
230
+ server.tool("git_stash_apply", "Apply stashed changes to the working directory. Applies changes from the specified stash to the current working directory, but keeps the stash in the stash list.", {
231
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
232
+ stashId: z.string().optional().default("stash@{0}").describe("Stash reference to apply (defaults to most recent stash)")
233
+ }, async ({ path, stashId }) => {
234
+ try {
235
+ const normalizedPath = PathValidation.normalizePath(path);
236
+ const gitService = new GitService(normalizedPath);
237
+ // Check if this is a git repository
238
+ const isRepo = await gitService.isGitRepository();
239
+ if (!isRepo) {
240
+ return {
241
+ content: [{
242
+ type: "text",
243
+ text: `Error: Not a Git repository: ${normalizedPath}`
244
+ }],
245
+ isError: true
246
+ };
247
+ }
248
+ const result = await gitService.applyStash(stashId);
249
+ if (!result.resultSuccessful) {
250
+ return {
251
+ content: [{
252
+ type: "text",
253
+ text: `Error: ${result.resultError.errorMessage}`
254
+ }],
255
+ isError: true
256
+ };
257
+ }
258
+ return {
259
+ content: [{
260
+ type: "text",
261
+ text: `Successfully applied stash: ${stashId}`
262
+ }]
263
+ };
264
+ }
265
+ catch (error) {
266
+ return {
267
+ content: [{
268
+ type: "text",
269
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
270
+ }],
271
+ isError: true
272
+ };
273
+ }
274
+ });
275
+ // Pop stash
276
+ server.tool("git_stash_pop", "Apply and remove a stash. Applies the specified stash to the working directory and then removes it from the stash stack. Combines the apply and drop operations.", {
277
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
278
+ stashId: z.string().optional().default("stash@{0}").describe("Stash reference to pop (defaults to most recent stash)")
279
+ }, async ({ path, stashId }) => {
280
+ try {
281
+ const normalizedPath = PathValidation.normalizePath(path);
282
+ const gitService = new GitService(normalizedPath);
283
+ // Check if this is a git repository
284
+ const isRepo = await gitService.isGitRepository();
285
+ if (!isRepo) {
286
+ return {
287
+ content: [{
288
+ type: "text",
289
+ text: `Error: Not a Git repository: ${normalizedPath}`
290
+ }],
291
+ isError: true
292
+ };
293
+ }
294
+ const result = await gitService.popStash(stashId);
295
+ if (!result.resultSuccessful) {
296
+ return {
297
+ content: [{
298
+ type: "text",
299
+ text: `Error: ${result.resultError.errorMessage}`
300
+ }],
301
+ isError: true
302
+ };
303
+ }
304
+ return {
305
+ content: [{
306
+ type: "text",
307
+ text: `Successfully popped stash: ${stashId}`
308
+ }]
309
+ };
310
+ }
311
+ catch (error) {
312
+ return {
313
+ content: [{
314
+ type: "text",
315
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
316
+ }],
317
+ isError: true
318
+ };
319
+ }
320
+ });
321
+ // Cherry-pick commits
322
+ server.tool("git_cherry_pick", "Apply changes from specific commits to the current branch. Takes the changes introduced in one or more existing commits and creates new commits with those changes on the current branch.", {
323
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
324
+ commits: z.array(z.string()).min(1, "At least one commit hash is required").describe("Array of commit hashes to cherry-pick")
325
+ }, async ({ path, commits }) => {
326
+ try {
327
+ const normalizedPath = PathValidation.normalizePath(path);
328
+ const gitService = new GitService(normalizedPath);
329
+ // Check if this is a git repository
330
+ const isRepo = await gitService.isGitRepository();
331
+ if (!isRepo) {
332
+ return {
333
+ content: [{
334
+ type: "text",
335
+ text: `Error: Not a Git repository: ${normalizedPath}`
336
+ }],
337
+ isError: true
338
+ };
339
+ }
340
+ const result = await gitService.cherryPick(commits);
341
+ if (!result.resultSuccessful) {
342
+ return {
343
+ content: [{
344
+ type: "text",
345
+ text: `Error: ${result.resultError.errorMessage}`
346
+ }],
347
+ isError: true
348
+ };
349
+ }
350
+ return {
351
+ content: [{
352
+ type: "text",
353
+ text: `Successfully cherry-picked ${commits.length} commit${commits.length > 1 ? 's' : ''}`
354
+ }]
355
+ };
356
+ }
357
+ catch (error) {
358
+ return {
359
+ content: [{
360
+ type: "text",
361
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
362
+ }],
363
+ isError: true
364
+ };
365
+ }
366
+ });
367
+ // Rebase
368
+ server.tool("git_rebase", "Reapply commits on top of another base commit. Takes all changes that were committed on one branch and replays them on another branch, providing a cleaner project history.", {
369
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
370
+ branch: z.string().min(1, "Branch to rebase onto is required").describe("Branch or reference to rebase onto"),
371
+ interactive: z.boolean().optional().default(false).describe("Whether to use interactive rebase mode")
372
+ }, async ({ path, branch, interactive }) => {
373
+ try {
374
+ const normalizedPath = PathValidation.normalizePath(path);
375
+ const gitService = new GitService(normalizedPath);
376
+ // Check if this is a git repository
377
+ const isRepo = await gitService.isGitRepository();
378
+ if (!isRepo) {
379
+ return {
380
+ content: [{
381
+ type: "text",
382
+ text: `Error: Not a Git repository: ${normalizedPath}`
383
+ }],
384
+ isError: true
385
+ };
386
+ }
387
+ const result = await gitService.rebase(branch, interactive);
388
+ if (!result.resultSuccessful) {
389
+ return {
390
+ content: [{
391
+ type: "text",
392
+ text: `Error: ${result.resultError.errorMessage}`
393
+ }],
394
+ isError: true
395
+ };
396
+ }
397
+ return {
398
+ content: [{
399
+ type: "text",
400
+ text: `Successfully rebased onto '${branch}'${interactive ? ' (interactive)' : ''}`
401
+ }]
402
+ };
403
+ }
404
+ catch (error) {
405
+ return {
406
+ content: [{
407
+ type: "text",
408
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
409
+ }],
410
+ isError: true
411
+ };
412
+ }
413
+ });
414
+ // Log commits
415
+ server.tool("git_log", "Show commit history. Displays a log of commits in reverse chronological order, optionally limited to a specific file's history or a maximum number of commits.", {
416
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
417
+ maxCount: z.number().positive().optional().default(50).describe("Maximum number of commits to display"),
418
+ file: z.string().optional().describe("Optional file path to show history for a specific file")
419
+ }, async ({ path, maxCount, file }) => {
420
+ try {
421
+ const normalizedPath = PathValidation.normalizePath(path);
422
+ const gitService = new GitService(normalizedPath);
423
+ // Check if this is a git repository
424
+ const isRepo = await gitService.isGitRepository();
425
+ if (!isRepo) {
426
+ return {
427
+ content: [{
428
+ type: "text",
429
+ text: `Error: Not a Git repository: ${normalizedPath}`
430
+ }],
431
+ isError: true
432
+ };
433
+ }
434
+ const result = await gitService.getLog({
435
+ maxCount,
436
+ file
437
+ });
438
+ if (!result.resultSuccessful) {
439
+ return {
440
+ content: [{
441
+ type: "text",
442
+ text: `Error: ${result.resultError.errorMessage}`
443
+ }],
444
+ isError: true
445
+ };
446
+ }
447
+ if (result.resultData.length === 0) {
448
+ return {
449
+ content: [{
450
+ type: "text",
451
+ text: `No commits found${file ? ` for file '${file}'` : ''}`
452
+ }]
453
+ };
454
+ }
455
+ // Format output
456
+ let output = `Commit history${file ? ` for file '${file}'` : ''} (showing up to ${maxCount} commits)\n\n`;
457
+ result.resultData.forEach(commit => {
458
+ output += `Commit: ${commit.hash}\n`;
459
+ output += `Author: ${commit.author} <${commit.authorEmail}>\n`;
460
+ output += `Date: ${commit.date.toISOString()}\n\n`;
461
+ output += ` ${commit.message}\n\n`;
462
+ });
463
+ return {
464
+ content: [{
465
+ type: "text",
466
+ text: output
467
+ }]
468
+ };
469
+ }
470
+ catch (error) {
471
+ return {
472
+ content: [{
473
+ type: "text",
474
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
475
+ }],
476
+ isError: true
477
+ };
478
+ }
479
+ });
480
+ // Show commit details
481
+ server.tool("git_show", "Show details of a specific commit. Displays the commit message, author, date, and the changes introduced by the commit including the diff.", {
482
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository"),
483
+ commitHash: z.string().min(1, "Commit hash is required").describe("Hash or reference of the commit to display")
484
+ }, async ({ path, commitHash }) => {
485
+ try {
486
+ const normalizedPath = PathValidation.normalizePath(path);
487
+ const gitService = new GitService(normalizedPath);
488
+ // Check if this is a git repository
489
+ const isRepo = await gitService.isGitRepository();
490
+ if (!isRepo) {
491
+ return {
492
+ content: [{
493
+ type: "text",
494
+ text: `Error: Not a Git repository: ${normalizedPath}`
495
+ }],
496
+ isError: true
497
+ };
498
+ }
499
+ const result = await gitService.showCommit(commitHash);
500
+ if (!result.resultSuccessful) {
501
+ return {
502
+ content: [{
503
+ type: "text",
504
+ text: `Error: ${result.resultError.errorMessage}`
505
+ }],
506
+ isError: true
507
+ };
508
+ }
509
+ return {
510
+ content: [{
511
+ type: "text",
512
+ text: result.resultData
513
+ }]
514
+ };
515
+ }
516
+ catch (error) {
517
+ return {
518
+ content: [{
519
+ type: "text",
520
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
521
+ }],
522
+ isError: true
523
+ };
524
+ }
525
+ });
526
+ }