@cyanheads/git-mcp-server 2.10.4 → 2.10.5

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +67 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-2.10.4-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.27.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg?style=flat-square)](https://github.com/cyanheads/git-mcp-server/issues) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.2.21-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-2.10.5-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.27.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg?style=flat-square)](https://github.com/cyanheads/git-mcp-server/issues) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.2.21-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
package/dist/index.js CHANGED
@@ -15357,7 +15357,7 @@ var package_default;
15357
15357
  var init_package = __esm(() => {
15358
15358
  package_default = {
15359
15359
  name: "@cyanheads/git-mcp-server",
15360
- version: "2.10.4",
15360
+ version: "2.10.5",
15361
15361
  mcpName: "io.github.cyanheads/git-mcp-server",
15362
15362
  description: "A secure and scalable Git MCP server enabling AI agents to perform comprehensive Git version control operations via STDIO and Streamable HTTP.",
15363
15363
  main: "dist/index.js",
@@ -136968,6 +136968,9 @@ class BaseGitProvider {
136968
136968
  }
136969
136969
  }
136970
136970
 
136971
+ // src/services/git/providers/cli/operations/core/init.ts
136972
+ import { mkdir } from "node:fs/promises";
136973
+
136971
136974
  // src/services/git/providers/cli/utils/command-builder.ts
136972
136975
  function buildGitCommand(config3) {
136973
136976
  const parts = [config3.command];
@@ -137567,9 +137570,9 @@ async function executeInit(options, context, execGit) {
137567
137570
  }
137568
137571
  const initialBranch = options.initialBranch || "main";
137569
137572
  args.push(`--initial-branch=${initialBranch}`);
137570
- args.push(options.path);
137573
+ await mkdir(options.path, { recursive: true }).catch(() => {});
137571
137574
  const cmd = buildGitCommand({ command: "init", args });
137572
- await execGit(cmd, context.workingDirectory, context.requestContext);
137575
+ await execGit(cmd, options.path, context.requestContext);
137573
137576
  const result = {
137574
137577
  success: true,
137575
137578
  path: options.path,
@@ -138009,8 +138012,10 @@ async function executeDiff(options, context, execGit) {
138009
138012
  });
138010
138013
  const statResult2 = await execGit(statCmd2, context.workingDirectory, context.requestContext);
138011
138014
  const stats2 = parseGitDiffStat(statResult2.stdout);
138012
- let untrackedStatOutput2 = "";
138013
138015
  let untrackedFileCount2 = 0;
138016
+ let untrackedAdditions2 = 0;
138017
+ let untrackedDeletions2 = 0;
138018
+ let untrackedStatOutput = "";
138014
138019
  if (options.includeUntracked) {
138015
138020
  let untrackedFiles = await getUntrackedFiles(execGit, context);
138016
138021
  if (options.excludePatterns?.length) {
@@ -138019,18 +138024,20 @@ async function executeDiff(options, context, execGit) {
138019
138024
  for (const file2 of untrackedFiles) {
138020
138025
  const result = await execUntrackedDiff(execGit, context, file2, true);
138021
138026
  if (result) {
138022
- untrackedStatOutput2 += result;
138027
+ const fileStat = parseGitDiffStat(result);
138028
+ untrackedAdditions2 += fileStat.totalAdditions;
138029
+ untrackedDeletions2 += fileStat.totalDeletions;
138030
+ untrackedStatOutput += result;
138023
138031
  untrackedFileCount2++;
138024
138032
  }
138025
138033
  }
138026
138034
  }
138027
- const untrackedStats2 = untrackedStatOutput2 ? parseGitDiffStat(untrackedStatOutput2) : { totalAdditions: 0, totalDeletions: 0 };
138028
138035
  return {
138029
- diff: statResult2.stdout + untrackedStatOutput2,
138036
+ diff: statResult2.stdout + untrackedStatOutput,
138030
138037
  filesChanged: stats2.files.length + untrackedFileCount2,
138031
- insertions: stats2.totalAdditions + untrackedStats2.totalAdditions,
138032
- deletions: stats2.totalDeletions + untrackedStats2.totalDeletions,
138033
- binary: statResult2.stdout.includes("Binary files") || untrackedStatOutput2.includes("Binary files"),
138038
+ insertions: stats2.totalAdditions + untrackedAdditions2,
138039
+ deletions: stats2.totalDeletions + untrackedDeletions2,
138040
+ binary: statResult2.stdout.includes("Binary files") || untrackedStatOutput.includes("Binary files"),
138034
138041
  ...excludedFiles && { excludedFiles }
138035
138042
  };
138036
138043
  }
@@ -138039,7 +138046,8 @@ async function executeDiff(options, context, execGit) {
138039
138046
  const diffResult = await execGit(diffCmd, context.workingDirectory, context.requestContext);
138040
138047
  let untrackedDiff = "";
138041
138048
  let untrackedFileCount = 0;
138042
- let untrackedStatOutput = "";
138049
+ let untrackedAdditions = 0;
138050
+ let untrackedDeletions = 0;
138043
138051
  if (options.includeUntracked) {
138044
138052
  let untrackedFiles = await getUntrackedFiles(execGit, context);
138045
138053
  if (options.excludePatterns?.length) {
@@ -138057,7 +138065,9 @@ async function executeDiff(options, context, execGit) {
138057
138065
  }
138058
138066
  const statResult2 = await execUntrackedDiff(execGit, context, file2, true);
138059
138067
  if (statResult2) {
138060
- untrackedStatOutput += statResult2;
138068
+ const fileStat = parseGitDiffStat(statResult2);
138069
+ untrackedAdditions += fileStat.totalAdditions;
138070
+ untrackedDeletions += fileStat.totalDeletions;
138061
138071
  }
138062
138072
  }
138063
138073
  }
@@ -138079,13 +138089,12 @@ async function executeDiff(options, context, execGit) {
138079
138089
  });
138080
138090
  const statResult = await execGit(statCmd, context.workingDirectory, context.requestContext);
138081
138091
  const stats = parseGitDiffStat(statResult.stdout);
138082
- const untrackedStats = untrackedStatOutput ? parseGitDiffStat(untrackedStatOutput) : { totalAdditions: 0, totalDeletions: 0 };
138083
138092
  const hasBinary = combinedDiff.includes("Binary files");
138084
138093
  return {
138085
138094
  diff: combinedDiff,
138086
138095
  filesChanged: stats.files.length + untrackedFileCount,
138087
- insertions: stats.totalAdditions + untrackedStats.totalAdditions,
138088
- deletions: stats.totalDeletions + untrackedStats.totalDeletions,
138096
+ insertions: stats.totalAdditions + untrackedAdditions,
138097
+ deletions: stats.totalDeletions + untrackedDeletions,
138089
138098
  binary: hasBinary,
138090
138099
  ...excludedFiles && { excludedFiles }
138091
138100
  };
@@ -138147,11 +138156,11 @@ async function executeBranch(options, context, execGit) {
138147
138156
  ].join(GIT_FIELD_DELIMITER);
138148
138157
  const refPrefixes = options.all ? ["refs/heads", "refs/remotes"] : [options.remote ? "refs/remotes" : "refs/heads"];
138149
138158
  args.push(`--format=${format}`, ...refPrefixes);
138150
- if (options.merged !== undefined) {
138159
+ if (options.merged !== undefined && options.merged !== false) {
138151
138160
  const mergedRef = typeof options.merged === "string" ? options.merged : "HEAD";
138152
138161
  args.push(`--merged=${mergedRef}`);
138153
138162
  }
138154
- if (options.noMerged !== undefined) {
138163
+ if (options.noMerged !== undefined && options.noMerged !== false) {
138155
138164
  const noMergedRef = typeof options.noMerged === "string" ? options.noMerged : "HEAD";
138156
138165
  args.push(`--no-merged=${noMergedRef}`);
138157
138166
  }
@@ -138239,7 +138248,7 @@ async function executeCheckout(options, context, execGit) {
138239
138248
  const cmd = buildGitCommand({ command: "checkout", args });
138240
138249
  const result = await execGit(cmd, context.workingDirectory, context.requestContext);
138241
138250
  const filesModified = result.stdout.split(`
138242
- `).filter((line) => line.trim() && !line.startsWith("Switched") && !line.startsWith("Already")).map((line) => line.trim());
138251
+ `).map((line) => line.trim()).filter((line) => line && !line.startsWith("Switched") && !line.startsWith("Already") && !line.startsWith("Your branch") && !line.startsWith("(use ") && !line.startsWith("HEAD is now"));
138243
138252
  const checkoutResult = {
138244
138253
  success: true,
138245
138254
  target: options.target,
@@ -138370,9 +138379,19 @@ async function executeRebase(options, context, execGit) {
138370
138379
  const match = line.match(/CONFLICT.*?in (.+)$/);
138371
138380
  return match?.[1] || "";
138372
138381
  }).filter((f3) => f3);
138373
- const applyingLines = result.stdout.split(`
138374
- `).filter((line) => line.startsWith("Applying:"));
138375
- const rebasedCommits = applyingLines.length;
138382
+ let rebasedCommits = 0;
138383
+ const progressLines = result.stderr.split(`
138384
+ `).filter((line) => /Rebasing \(\d+\/\d+\)/.test(line));
138385
+ if (progressLines.length > 0) {
138386
+ const lastProgress = progressLines.at(-1);
138387
+ const progressMatch = lastProgress.match(/Rebasing \((\d+)\/(\d+)\)/);
138388
+ if (progressMatch) {
138389
+ rebasedCommits = parseInt(progressMatch[2], 10);
138390
+ }
138391
+ } else {
138392
+ rebasedCommits = result.stdout.split(`
138393
+ `).filter((line) => line.startsWith("Applying:")).length;
138394
+ }
138376
138395
  const rebaseResult = {
138377
138396
  success: !hasConflicts,
138378
138397
  conflicts: hasConflicts,
@@ -138678,7 +138697,7 @@ async function executePull(options, context, execGit) {
138678
138697
  }
138679
138698
  const hasConflicts = result.stdout.includes("CONFLICT") || result.stderr.includes("CONFLICT");
138680
138699
  const filesChanged = result.stdout.split(`
138681
- `).filter((line) => line.trim() && !line.includes("CONFLICT")).map((line) => line.trim()).filter((f3) => f3);
138700
+ `).map((line) => line.trim()).filter((line) => line && !line.includes("CONFLICT") && !line.startsWith("Already up to date") && !line.startsWith("From ") && !line.startsWith("Your branch") && !line.startsWith("(use ") && !line.startsWith("Updating ") && !line.startsWith("Fast-forward") && !line.match(/^\d+ files? changed/));
138682
138701
  const pullResult = {
138683
138702
  success: !hasConflicts,
138684
138703
  remote,
@@ -138702,7 +138721,7 @@ async function executeTag(options, context, execGit) {
138702
138721
  "%(refname:short)",
138703
138722
  "%(if)%(*objectname:short)%(then)%(*objectname:short)%(else)%(objectname:short)%(end)",
138704
138723
  "%(if)%(contents:subject)%(then)%(contents:subject)%(end)",
138705
- "%(if)%(taggername)%(then)%(taggername) <%(taggeremail)>%(end)",
138724
+ "%(if)%(taggername)%(then)%(taggername) %(taggeremail)%(end)",
138706
138725
  "%(if)%(creatordate:unix)%(then)%(creatordate:unix)%(end)"
138707
138726
  ].join(GIT_FIELD_DELIMITER);
138708
138727
  const refCmd = buildGitCommand({
@@ -138753,10 +138772,14 @@ async function executeTag(options, context, execGit) {
138753
138772
  }
138754
138773
  return createArgs;
138755
138774
  };
138756
- const createCmd = buildGitCommand({
138757
- command: "tag",
138758
- args: buildCreateArgs(shouldSign)
138759
- });
138775
+ const configOverride = shouldSign ? [] : ["-c", "tag.gpgSign=false"];
138776
+ const createCmd = [
138777
+ ...configOverride,
138778
+ ...buildGitCommand({
138779
+ command: "tag",
138780
+ args: buildCreateArgs(shouldSign)
138781
+ })
138782
+ ];
138760
138783
  try {
138761
138784
  await execGit(createCmd, context.workingDirectory, context.requestContext);
138762
138785
  } catch (error48) {
@@ -138810,7 +138833,8 @@ async function executeStash(options, context, execGit) {
138810
138833
  const parts = line.split("\t");
138811
138834
  const [refPart, tsPart, ...subjectParts] = parts;
138812
138835
  if (refPart && tsPart && subjectParts.length > 0) {
138813
- const stashIndex = parseInt(refPart, 10);
138836
+ const indexMatch = refPart.match(/\{(\d+)\}/);
138837
+ const stashIndex = indexMatch ? parseInt(indexMatch[1], 10) : index;
138814
138838
  const timestamp = parseInt(tsPart, 10);
138815
138839
  const subject = subjectParts.join("\t");
138816
138840
  const branchMatch = subject.match(/^(?:WIP on|On)\s+([^:]+):\s+(.*)$/);
@@ -149966,12 +149990,13 @@ var TOOL_TITLE3 = "Git Clean";
149966
149990
  var TOOL_DESCRIPTION3 = "Remove untracked files from the working directory. Requires force flag for safety. Use dry-run to preview files that would be removed.";
149967
149991
  var InputSchema3 = exports_external.object({
149968
149992
  path: PathSchema,
149969
- force: ForceSchema.refine((val) => val === true, {
149970
- message: "force flag must be set to true to clean untracked files"
149971
- }),
149993
+ force: ForceSchema,
149972
149994
  dryRun: DryRunSchema,
149973
149995
  directories: exports_external.boolean().default(false).describe("Remove untracked directories in addition to files."),
149974
149996
  ignored: exports_external.boolean().default(false).describe("Remove ignored files as well.")
149997
+ }).refine((data) => data.force === true || data.dryRun === true, {
149998
+ message: "force flag must be set to true to clean untracked files (or use dryRun to preview)",
149999
+ path: ["force"]
149975
150000
  });
149976
150001
  var OutputSchema4 = exports_external.object({
149977
150002
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
@@ -150456,7 +150481,7 @@ var InputSchema9 = exports_external.object({
150456
150481
  var OutputSchema10 = exports_external.object({
150457
150482
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
150458
150483
  currentBranch: exports_external.string().nullable().describe("Current branch name."),
150459
- isClean: exports_external.boolean().describe("True if working directory is clean."),
150484
+ isClean: exports_external.boolean().describe("True if working directory is clean (no staged, unstaged, or untracked changes). When includeUntracked is false, untracked files are excluded from this check."),
150460
150485
  stagedChanges: exports_external.object({
150461
150486
  added: exports_external.array(exports_external.string()).optional().describe("Files added to the index (staged)."),
150462
150487
  modified: exports_external.array(exports_external.string()).optional().describe("Files modified and staged."),
@@ -150730,10 +150755,15 @@ async function gitAddLogic(input, { provider, targetPath, appContext }) {
150730
150755
  requestContext: appContext,
150731
150756
  tenantId: appContext.tenantId || "default-tenant"
150732
150757
  });
150758
+ const stagedFiles = result.stagedFiles.length > 0 ? result.stagedFiles : [
150759
+ ...statusResult.stagedChanges.added || [],
150760
+ ...statusResult.stagedChanges.modified || [],
150761
+ ...statusResult.stagedChanges.deleted || []
150762
+ ];
150733
150763
  return {
150734
150764
  success: result.success,
150735
- stagedFiles: result.stagedFiles,
150736
- totalFiles: result.stagedFiles.length,
150765
+ stagedFiles,
150766
+ totalFiles: stagedFiles.length,
150737
150767
  status: {
150738
150768
  current_branch: statusResult.currentBranch,
150739
150769
  staged_changes: flattenChanges(statusResult.stagedChanges),
@@ -151130,7 +151160,7 @@ var TOOL_DESCRIPTION15 = "Show details of a git object (commit, tree, blob, or t
151130
151160
  var InputSchema15 = exports_external.object({
151131
151161
  path: PathSchema,
151132
151162
  object: CommitRefSchema.describe("Git object to show (commit hash, branch, tag, tree, or blob)."),
151133
- format: exports_external.enum(["raw", "json"]).optional().describe("Output format for the git object."),
151163
+ format: exports_external.enum(["raw"]).optional().describe('Output format for the git object. Use "raw" for unprocessed git output.'),
151134
151164
  stat: exports_external.boolean().default(false).describe("Show diffstat instead of full diff."),
151135
151165
  filePath: exports_external.string().optional().describe("View specific file at a given commit reference. When provided, shows the file content from the specified object.")
151136
151166
  });
@@ -151213,8 +151243,8 @@ var InputSchema16 = exports_external.object({
151213
151243
  force: ForceSchema,
151214
151244
  all: AllSchema.describe("For list operation: show both local and remote branches."),
151215
151245
  remote: exports_external.boolean().default(false).describe("For list operation: show only remote branches."),
151216
- merged: exports_external.union([exports_external.boolean(), CommitRefSchema]).optional().describe("For list operation: show only branches merged into HEAD (true) or specified commit (string)."),
151217
- noMerged: exports_external.union([exports_external.boolean(), CommitRefSchema]).optional().describe("For list operation: show only branches not merged into HEAD (true) or specified commit (string).")
151246
+ merged: exports_external.preprocess((val) => val === "true" ? true : val === "false" ? false : val, exports_external.union([exports_external.boolean(), CommitRefSchema])).optional().describe("For list operation: show only branches merged into HEAD (true) or specified commit (string)."),
151247
+ noMerged: exports_external.preprocess((val) => val === "true" ? true : val === "false" ? false : val, exports_external.union([exports_external.boolean(), CommitRefSchema])).optional().describe("For list operation: show only branches not merged into HEAD (true) or specified commit (string).")
151218
151248
  });
151219
151249
  var BranchInfoSchema = exports_external.object({
151220
151250
  name: exports_external.string().describe("Branch name."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/git-mcp-server",
3
- "version": "2.10.4",
3
+ "version": "2.10.5",
4
4
  "mcpName": "io.github.cyanheads/git-mcp-server",
5
5
  "description": "A secure and scalable Git MCP server enabling AI agents to perform comprehensive Git version control operations via STDIO and Streamable HTTP.",
6
6
  "main": "dist/index.js",