@cyanheads/git-mcp-server 2.8.0 → 2.8.2

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 +109 -23
  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.8.0-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.26.0-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-^5.9.3-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.8.2-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.26.0-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-^5.9.3-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
@@ -15335,7 +15335,7 @@ var package_default;
15335
15335
  var init_package = __esm(() => {
15336
15336
  package_default = {
15337
15337
  name: "@cyanheads/git-mcp-server",
15338
- version: "2.7.1",
15338
+ version: "2.8.2",
15339
15339
  mcpName: "io.github.cyanheads/git-mcp-server",
15340
15340
  description: "A secure and scalable Git MCP server enabling AI agents to perform comprehensive Git version control operations via STDIO and Streamable HTTP.",
15341
15341
  main: "dist/index.js",
@@ -15855,9 +15855,9 @@ var init_config = __esm(() => {
15855
15855
  git: exports_external.object({
15856
15856
  provider: exports_external.preprocess(emptyStringAsUndefined, exports_external.enum(["auto", "cli", "isomorphic"]).default("auto")),
15857
15857
  signCommits: exports_external.coerce.boolean().default(false),
15858
- authorName: exports_external.string().optional(),
15858
+ authorName: exports_external.string().regex(/^[^\n\r\0]*$/, "Git author name must not contain newlines or null bytes").optional(),
15859
15859
  authorEmail: exports_external.string().email().optional(),
15860
- committerName: exports_external.string().optional(),
15860
+ committerName: exports_external.string().regex(/^[^\n\r\0]*$/, "Git committer name must not contain newlines or null bytes").optional(),
15861
15861
  committerEmail: exports_external.string().email().optional(),
15862
15862
  wrapupInstructionsPath: exports_external.preprocess(expandTildePath, exports_external.string().optional()),
15863
15863
  baseDir: exports_external.preprocess((val) => expandTildePath(emptyStringAsUndefined(val)), exports_external.string().refine((p) => !p || isAbsolutePath(p), {
@@ -176201,7 +176201,20 @@ async function executeCommit(options, context, execGit) {
176201
176201
  args.push(`--author=${authorStr}`);
176202
176202
  }
176203
176203
  const cmd = buildGitCommand({ command: "commit", args });
176204
- await execGit(cmd, context.workingDirectory, context.requestContext);
176204
+ try {
176205
+ await execGit(cmd, context.workingDirectory, context.requestContext);
176206
+ } catch (error48) {
176207
+ if (shouldSign && options.forceUnsignedOnFailure) {
176208
+ const unsignedArgs = args.filter((a) => a !== "--gpg-sign");
176209
+ const unsignedCmd = buildGitCommand({
176210
+ command: "commit",
176211
+ args: unsignedArgs
176212
+ });
176213
+ await execGit(unsignedCmd, context.workingDirectory, context.requestContext);
176214
+ } else {
176215
+ throw error48;
176216
+ }
176217
+ }
176205
176218
  const hashCmd = buildGitCommand({
176206
176219
  command: "rev-parse",
176207
176220
  args: ["HEAD"]
@@ -177015,22 +177028,40 @@ async function executeTag(options, context, execGit) {
177015
177028
  if (!options.tagName) {
177016
177029
  throw new Error("Tag name is required for create operation");
177017
177030
  }
177018
- args.push(options.tagName);
177019
177031
  const shouldSign = options.sign ?? shouldSignCommits();
177020
- if (shouldSign) {
177021
- const message = options.message || `Tag ${options.tagName}`;
177022
- args.push("-s", "-m", message);
177023
- } else if (options.message && options.annotated) {
177024
- args.push("-a", "-m", options.message);
177025
- }
177026
- if (options.commit) {
177027
- args.push(options.commit);
177028
- }
177029
- if (options.force) {
177030
- args.push("--force");
177032
+ const buildCreateArgs = (sign) => {
177033
+ const createArgs = [options.tagName];
177034
+ if (sign) {
177035
+ const message = options.message || `Tag ${options.tagName}`;
177036
+ createArgs.push("-s", "-m", message);
177037
+ } else if (options.message && options.annotated) {
177038
+ createArgs.push("-a", "-m", options.message);
177039
+ }
177040
+ if (options.commit) {
177041
+ createArgs.push(options.commit);
177042
+ }
177043
+ if (options.force) {
177044
+ createArgs.push("--force");
177045
+ }
177046
+ return createArgs;
177047
+ };
177048
+ const createCmd = buildGitCommand({
177049
+ command: "tag",
177050
+ args: buildCreateArgs(shouldSign)
177051
+ });
177052
+ try {
177053
+ await execGit(createCmd, context.workingDirectory, context.requestContext);
177054
+ } catch (error48) {
177055
+ if (shouldSign && options.forceUnsignedOnFailure) {
177056
+ const unsignedCmd = buildGitCommand({
177057
+ command: "tag",
177058
+ args: buildCreateArgs(false)
177059
+ });
177060
+ await execGit(unsignedCmd, context.workingDirectory, context.requestContext);
177061
+ } else {
177062
+ throw error48;
177063
+ }
177031
177064
  }
177032
- const cmd = buildGitCommand({ command: "tag", args });
177033
- await execGit(cmd, context.workingDirectory, context.requestContext);
177034
177065
  const createResult = {
177035
177066
  mode: "create",
177036
177067
  created: options.tagName
@@ -194026,6 +194057,25 @@ async function resolveWorkingDirectory(pathInput, appContext, storage) {
194026
194057
  });
194027
194058
  return sanitizedPath;
194028
194059
  }
194060
+ var DEFAULT_PROTECTION = {
194061
+ protectedBranches: ["main", "master", "production", "prod", "develop", "dev"],
194062
+ enforce: true
194063
+ };
194064
+ function isProtectedBranch(branchName, config3 = DEFAULT_PROTECTION) {
194065
+ return config3.protectedBranches.includes(branchName.toLowerCase());
194066
+ }
194067
+ function validateProtectedBranchOperation(branchName, operation, confirmed, config3 = DEFAULT_PROTECTION) {
194068
+ if (!config3.enforce) {
194069
+ return;
194070
+ }
194071
+ if (isProtectedBranch(branchName, config3) && !confirmed) {
194072
+ throw new McpError(-32007 /* ValidationError */, `Cannot perform '${operation}' on protected branch '${branchName}' without explicit confirmation.`, {
194073
+ branch: branchName,
194074
+ operation,
194075
+ hint: "Set the confirmation parameter to true to proceed."
194076
+ });
194077
+ }
194078
+ }
194029
194079
 
194030
194080
  // src/mcp-server/tools/utils/toolHandlerFactory.ts
194031
194081
  function validateSdkContext(ctx) {
@@ -196212,7 +196262,8 @@ var InputSchema23 = exports_external.object({
196212
196262
  tags: exports_external.boolean().default(false).describe("Push all tags to the remote."),
196213
196263
  dryRun: DryRunSchema,
196214
196264
  delete: exports_external.boolean().default(false).describe("Delete the specified remote branch."),
196215
- remoteBranch: BranchNameSchema.optional().describe("Remote branch name to push to (if different from local branch name).")
196265
+ remoteBranch: BranchNameSchema.optional().describe("Remote branch name to push to (if different from local branch name)."),
196266
+ confirmed: exports_external.boolean().default(false).describe("Explicit confirmation required for force push or branch deletion on protected branches (main, master, production, etc.).")
196216
196267
  });
196217
196268
  var OutputSchema24 = exports_external.object({
196218
196269
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
@@ -196223,6 +196274,21 @@ var OutputSchema24 = exports_external.object({
196223
196274
  rejectedRefs: exports_external.array(exports_external.string()).describe("References that were rejected by the remote.")
196224
196275
  });
196225
196276
  async function gitPushLogic(input, { provider, targetPath, appContext }) {
196277
+ if (input.force || input.delete) {
196278
+ let branchToCheck = input.branch;
196279
+ if (!branchToCheck) {
196280
+ const status = await provider.status({ includeUntracked: false }, {
196281
+ workingDirectory: targetPath,
196282
+ requestContext: appContext,
196283
+ tenantId: appContext.tenantId || "default-tenant"
196284
+ });
196285
+ branchToCheck = status?.currentBranch ?? undefined;
196286
+ }
196287
+ if (branchToCheck) {
196288
+ const operation = input.force ? "force push" : "branch deletion";
196289
+ validateProtectedBranchOperation(branchToCheck, operation, input.confirmed);
196290
+ }
196291
+ }
196226
196292
  const pushOptions = {};
196227
196293
  if (input.remote !== undefined) {
196228
196294
  pushOptions.remote = input.remote;
@@ -196383,7 +196449,8 @@ var InputSchema25 = exports_external.object({
196383
196449
  path: PathSchema,
196384
196450
  mode: exports_external.enum(["soft", "mixed", "hard", "merge", "keep"]).default("mixed").describe("Reset mode: soft (keep changes staged), mixed (unstage changes), hard (discard all changes), merge (reset and merge), keep (reset but keep local changes)."),
196385
196451
  target: CommitRefSchema.optional().describe("Target commit to reset to (default: HEAD)."),
196386
- paths: exports_external.array(exports_external.string()).optional().describe("Specific file paths to reset (leaves HEAD unchanged).")
196452
+ paths: exports_external.array(exports_external.string()).optional().describe("Specific file paths to reset (leaves HEAD unchanged)."),
196453
+ confirmed: exports_external.boolean().default(false).describe("Explicit confirmation required for hard reset on protected branches (main, master, production, etc.).")
196387
196454
  });
196388
196455
  var OutputSchema26 = exports_external.object({
196389
196456
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
@@ -196392,6 +196459,16 @@ var OutputSchema26 = exports_external.object({
196392
196459
  filesReset: exports_external.array(exports_external.string()).describe("Files that were affected by the reset.")
196393
196460
  });
196394
196461
  async function gitResetLogic(input, { provider, targetPath, appContext }) {
196462
+ if (input.mode === "hard") {
196463
+ const status = await provider.status({ includeUntracked: false }, {
196464
+ workingDirectory: targetPath,
196465
+ requestContext: appContext,
196466
+ tenantId: appContext.tenantId || "default-tenant"
196467
+ });
196468
+ if (status?.currentBranch) {
196469
+ validateProtectedBranchOperation(status.currentBranch, "reset --hard", input.confirmed);
196470
+ }
196471
+ }
196395
196472
  const resetOptions = {
196396
196473
  mode: input.mode
196397
196474
  };
@@ -196532,6 +196609,8 @@ var InputSchema27 = exports_external.object({
196532
196609
  commit: CommitRefSchema.optional().describe("Commit to tag (default: HEAD for create operation)."),
196533
196610
  message: exports_external.string().optional().describe("Tag message (creates annotated tag)."),
196534
196611
  annotated: exports_external.boolean().default(false).describe("Create annotated tag with message."),
196612
+ sign: SignSchema,
196613
+ forceUnsignedOnFailure: exports_external.boolean().default(false).describe("If GPG/SSH signing fails, retry the tag creation without signing instead of failing."),
196535
196614
  force: ForceSchema.describe("Force tag creation/deletion (overwrite existing).")
196536
196615
  });
196537
196616
  var TagInfoSchema = exports_external.object({
@@ -196550,7 +196629,8 @@ var OutputSchema28 = exports_external.object({
196550
196629
  });
196551
196630
  async function gitTagLogic(input, { provider, targetPath, appContext }) {
196552
196631
  const tagOptions = {
196553
- mode: input.mode
196632
+ mode: input.mode,
196633
+ forceUnsignedOnFailure: input.forceUnsignedOnFailure
196554
196634
  };
196555
196635
  if (input.tagName !== undefined) {
196556
196636
  tagOptions.tagName = input.tagName;
@@ -196564,6 +196644,9 @@ async function gitTagLogic(input, { provider, targetPath, appContext }) {
196564
196644
  if (input.annotated !== undefined) {
196565
196645
  tagOptions.annotated = input.annotated;
196566
196646
  }
196647
+ if (input.sign !== undefined) {
196648
+ tagOptions.sign = input.sign;
196649
+ }
196567
196650
  if (input.force !== undefined) {
196568
196651
  tagOptions.force = input.force;
196569
196652
  }
@@ -202061,7 +202144,11 @@ function createHttpApp(mcpServer, parentContext) {
202061
202144
  ...transportContext,
202062
202145
  staleTimeoutMs: config2.mcpStatefulSessionStaleTimeoutMs
202063
202146
  });
202064
- const allowedOrigin = Array.isArray(config2.mcpAllowedOrigins) && config2.mcpAllowedOrigins.length > 0 ? config2.mcpAllowedOrigins : "*";
202147
+ const explicitOrigins = config2.mcpAllowedOrigins;
202148
+ const allowedOrigin = explicitOrigins && explicitOrigins.length > 0 ? explicitOrigins : "*";
202149
+ if (allowedOrigin === "*" && config2.environment === "production") {
202150
+ logger.warning("MCP_ALLOWED_ORIGINS is not configured. CORS will allow all origins. " + "Set MCP_ALLOWED_ORIGINS to restrict cross-origin access in production.", transportContext);
202151
+ }
202065
202152
  app.use("*", cors({
202066
202153
  origin: allowedOrigin,
202067
202154
  allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
@@ -202095,7 +202182,6 @@ function createHttpApp(mcpServer, parentContext) {
202095
202182
  name: config2.mcpServerName,
202096
202183
  version: config2.mcpServerVersion,
202097
202184
  description: config2.mcpServerDescription,
202098
- environment: config2.environment,
202099
202185
  transport: config2.mcpTransportType,
202100
202186
  sessionMode: config2.mcpSessionMode
202101
202187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/git-mcp-server",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
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",