@cyanheads/git-mcp-server 2.12.0 → 2.13.0

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 +35 -35
  2. package/dist/index.js +114 -101
  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.12.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.29.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-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.11-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-2.13.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.29.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-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.11-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -83,20 +83,20 @@ For Streamable HTTP, set `MCP_TRANSPORT_TYPE=http` and `MCP_HTTP_PORT=3015`.
83
83
 
84
84
  Built on [`mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template).
85
85
 
86
- | Feature | Details |
87
- | :--------------------------- | :------------------------------------------------------------------------------------------------------------------ |
88
- | Declarative tools | Define capabilities in single, self-contained files. The framework handles registration, validation, and execution. |
89
- | Error handling | Unified `McpError` system for consistent, structured error responses. |
90
- | Authentication | Supports `none`, `jwt`, and `oauth` modes. |
91
- | Pluggable storage | Swap backends (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2`) without changing business logic. |
92
- | Observability | Structured logging (Pino) and optional auto-instrumented OpenTelemetry for traces and metrics. |
93
- | Dependency injection | Built with `tsyringe` for decoupled, testable architecture. |
94
- | Cross-runtime | Auto-detects Bun or Node.js and uses the appropriate process spawning method. |
95
- | Provider architecture | Pluggable git provider system. Current: CLI. Planned: isomorphic-git for edge deployment. |
96
- | Working directory management | Session-specific directory context for multi-repo workflows. |
97
- | Configurable git identity | Override author/committer info via environment variables, with fallback to global git config. |
98
- | Commit signing | Optional GPG/SSH signing for commits, merges, rebases, cherry-picks, and tags. |
99
- | Safety | Destructive operations (`git clean`, `git reset --hard`) require explicit confirmation flags. |
86
+ | Feature | Details |
87
+ | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
88
+ | Declarative tools | Define capabilities in single, self-contained files. The framework handles registration, validation, and execution. |
89
+ | Error handling | Unified `McpError` system for consistent, structured error responses. |
90
+ | Authentication | Supports `none`, `jwt`, and `oauth` modes. |
91
+ | Pluggable storage | Swap backends (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2`) without changing business logic. |
92
+ | Observability | Structured logging (Pino) and optional auto-instrumented OpenTelemetry for traces and metrics. |
93
+ | Dependency injection | Built with `tsyringe` for decoupled, testable architecture. |
94
+ | Cross-runtime | Auto-detects Bun or Node.js and uses the appropriate process spawning method. |
95
+ | Provider architecture | Pluggable git provider system. Current: CLI. Planned: isomorphic-git for edge deployment. |
96
+ | Working directory management | Session-specific directory context for multi-repo workflows. |
97
+ | Configurable git identity | Override author/committer info via environment variables, with fallback to global git config. |
98
+ | Commit signing | GPG/SSH signing (enabled by default) for commits, merges, rebases, cherry-picks, and tags. Silent fallback to unsigned on failure with `signed`/`signingWarning` fields in responses. |
99
+ | Safety | Destructive operations (`git clean`, `git reset --hard`) require explicit confirmation flags. |
100
100
 
101
101
  ## Security
102
102
 
@@ -111,26 +111,26 @@ Built on [`mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template).
111
111
 
112
112
  All configuration is validated at startup in `src/config/index.ts`. Key environment variables:
113
113
 
114
- | Variable | Description | Default |
115
- | :----------------------------- | :----------------------------------------------------------------------------------------- | :---------- |
116
- | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
117
- | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `auto` |
118
- | `MCP_RESPONSE_FORMAT` | Response format: `json` (LLM-optimized), `markdown` (human-readable), or `auto`. | `json` |
119
- | `MCP_RESPONSE_VERBOSITY` | Detail level: `minimal`, `standard`, or `full`. | `standard` |
120
- | `MCP_HTTP_PORT` | HTTP server port. | `3015` |
121
- | `MCP_HTTP_HOST` | HTTP server hostname. | `127.0.0.1` |
122
- | `MCP_HTTP_ENDPOINT_PATH` | MCP request endpoint path. | `/mcp` |
123
- | `MCP_AUTH_MODE` | Authentication mode: `none`, `jwt`, or `oauth`. | `none` |
124
- | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv`, `r2`. | `in-memory` |
125
- | `OTEL_ENABLED` | Enable OpenTelemetry. | `false` |
126
- | `MCP_LOG_LEVEL` | Minimum log level: `debug`, `info`, `warn`, `error`. | `info` |
127
- | `GIT_SIGN_COMMITS` | Enable GPG/SSH signing for commits, merges, rebases, cherry-picks, and tags. | `false` |
128
- | `GIT_AUTHOR_NAME` | Git author name. Aliases: `GIT_USERNAME`, `GIT_USER`. Falls back to global git config. | `(none)` |
129
- | `GIT_AUTHOR_EMAIL` | Git author email. Aliases: `GIT_EMAIL`, `GIT_USER_EMAIL`. Falls back to global git config. | `(none)` |
130
- | `GIT_BASE_DIR` | Absolute path to restrict all git operations to a specific directory tree. | `(none)` |
131
- | `GIT_WRAPUP_INSTRUCTIONS_PATH` | Path to custom markdown file with workflow instructions. | `(none)` |
132
- | `MCP_AUTH_SECRET_KEY` | Required for `jwt` auth. 32+ character secret key. | `(none)` |
133
- | `OAUTH_ISSUER_URL` | Required for `oauth` auth. OIDC provider URL. | `(none)` |
114
+ | Variable | Description | Default |
115
+ | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | :---------- |
116
+ | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
117
+ | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `auto` |
118
+ | `MCP_RESPONSE_FORMAT` | Response format: `json` (LLM-optimized), `markdown` (human-readable), or `auto`. | `json` |
119
+ | `MCP_RESPONSE_VERBOSITY` | Detail level: `minimal`, `standard`, or `full`. | `standard` |
120
+ | `MCP_HTTP_PORT` | HTTP server port. | `3015` |
121
+ | `MCP_HTTP_HOST` | HTTP server hostname. | `127.0.0.1` |
122
+ | `MCP_HTTP_ENDPOINT_PATH` | MCP request endpoint path. | `/mcp` |
123
+ | `MCP_AUTH_MODE` | Authentication mode: `none`, `jwt`, or `oauth`. | `none` |
124
+ | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv`, `r2`. | `in-memory` |
125
+ | `OTEL_ENABLED` | Enable OpenTelemetry. | `false` |
126
+ | `MCP_LOG_LEVEL` | Minimum log level: `debug`, `info`, `warn`, `error`. | `info` |
127
+ | `GIT_SIGN_COMMITS` | GPG/SSH signing for commits, merges, rebases, cherry-picks, and tags. Falls back to unsigned on failure (see response `signed`/`signingWarning`). | `true` |
128
+ | `GIT_AUTHOR_NAME` | Git author name. Aliases: `GIT_USERNAME`, `GIT_USER`. Falls back to global git config. | `(none)` |
129
+ | `GIT_AUTHOR_EMAIL` | Git author email. Aliases: `GIT_EMAIL`, `GIT_USER_EMAIL`. Falls back to global git config. | `(none)` |
130
+ | `GIT_BASE_DIR` | Absolute path to restrict all git operations to a specific directory tree. | `(none)` |
131
+ | `GIT_WRAPUP_INSTRUCTIONS_PATH` | Path to custom markdown file with workflow instructions. | `(none)` |
132
+ | `MCP_AUTH_SECRET_KEY` | Required for `jwt` auth. 32+ character secret key. | `(none)` |
133
+ | `OAUTH_ISSUER_URL` | Required for `oauth` auth. OIDC provider URL. | `(none)` |
134
134
 
135
135
  ## Running the server
136
136
 
package/dist/index.js CHANGED
@@ -15284,7 +15284,7 @@ var package_default;
15284
15284
  var init_package = __esm(() => {
15285
15285
  package_default = {
15286
15286
  name: "@cyanheads/git-mcp-server",
15287
- version: "2.12.0",
15287
+ version: "2.13.0",
15288
15288
  mcpName: "io.github.cyanheads/git-mcp-server",
15289
15289
  description: "A secure and scalable Git MCP server enabling AI agents to perform comprehensive Git version control operations via STDIO and Streamable HTTP.",
15290
15290
  main: "dist/index.js",
@@ -15517,6 +15517,17 @@ var import_dotenv, packageManifest, emptyStringAsUndefined = (val) => {
15517
15517
  return;
15518
15518
  }
15519
15519
  return val;
15520
+ }, parseBoolEnv = (defaultValue) => (val) => {
15521
+ if (typeof val === "boolean")
15522
+ return val;
15523
+ if (typeof val === "string") {
15524
+ const lower = val.trim().toLowerCase();
15525
+ if (["true", "1", "yes", "on"].includes(lower))
15526
+ return true;
15527
+ if (["false", "0", "no", "off", ""].includes(lower))
15528
+ return false;
15529
+ }
15530
+ return defaultValue;
15520
15531
  }, expandTildePath = (path2) => {
15521
15532
  if (typeof path2 !== "string" || path2.trim() === "") {
15522
15533
  return;
@@ -15737,7 +15748,7 @@ var init_config = __esm(() => {
15737
15748
  }),
15738
15749
  git: exports_external.object({
15739
15750
  provider: exports_external.preprocess(emptyStringAsUndefined, exports_external.enum(["auto", "cli", "isomorphic"]).default("auto")),
15740
- signCommits: exports_external.coerce.boolean().default(false),
15751
+ signCommits: exports_external.preprocess(parseBoolEnv(true), exports_external.boolean()),
15741
15752
  authorName: exports_external.string().regex(/^[^\n\r\0]*$/, "Git author name must not contain newlines or null bytes").optional(),
15742
15753
  authorEmail: exports_external.string().email().optional(),
15743
15754
  committerName: exports_external.string().regex(/^[^\n\r\0]*$/, "Git committer name must not contain newlines or null bytes").optional(),
@@ -133658,6 +133669,7 @@ async function listDirtyFiles(execGit, cwd, ctx) {
133658
133669
  return files;
133659
133670
  }
133660
133671
  // src/services/git/providers/cli/operations/commits/commit.ts
133672
+ init_utils();
133661
133673
  async function executeCommit(options, context, execGit) {
133662
133674
  try {
133663
133675
  if (options.filesToStage?.length) {
@@ -133677,8 +133689,10 @@ async function executeCommit(options, context, execGit) {
133677
133689
  if (options.noVerify) {
133678
133690
  args.push("--no-verify");
133679
133691
  }
133680
- const shouldSign = options.sign ?? shouldSignCommits();
133681
- if (shouldSign) {
133692
+ const signRequested = shouldSignCommits();
133693
+ let signed = false;
133694
+ let signingWarning;
133695
+ if (signRequested) {
133682
133696
  args.push("--gpg-sign");
133683
133697
  }
133684
133698
  if (options.author) {
@@ -133688,17 +133702,20 @@ async function executeCommit(options, context, execGit) {
133688
133702
  const cmd = buildGitCommand({ command: "commit", args });
133689
133703
  try {
133690
133704
  await execGit(cmd, context.workingDirectory, context.requestContext);
133705
+ signed = signRequested;
133691
133706
  } catch (error48) {
133692
- if (shouldSign && options.forceUnsignedOnFailure) {
133693
- const unsignedArgs = args.filter((a) => a !== "--gpg-sign");
133694
- const unsignedCmd = buildGitCommand({
133695
- command: "commit",
133696
- args: unsignedArgs
133697
- });
133698
- await execGit(unsignedCmd, context.workingDirectory, context.requestContext);
133699
- } else {
133707
+ if (!signRequested) {
133700
133708
  throw error48;
133701
133709
  }
133710
+ const errorMessage = error48 instanceof Error ? error48.message : String(error48);
133711
+ logger.warning("Commit signing failed; retrying unsigned. Set GIT_SIGN_COMMITS=false to suppress this attempt.", { ...context.requestContext, error: error48 });
133712
+ signingWarning = `GIT_SIGN_COMMITS is enabled but signing failed; commit created unsigned. Check signing key availability (gpg-agent running, SSH key accessible). Underlying error: ${errorMessage}`;
133713
+ const unsignedArgs = args.filter((a) => a !== "--gpg-sign");
133714
+ const unsignedCmd = buildGitCommand({
133715
+ command: "commit",
133716
+ args: unsignedArgs
133717
+ });
133718
+ await execGit(unsignedCmd, context.workingDirectory, context.requestContext);
133702
133719
  }
133703
133720
  const hashCmd = buildGitCommand({
133704
133721
  command: "rev-parse",
@@ -133727,8 +133744,12 @@ async function executeCommit(options, context, execGit) {
133727
133744
  message: options.message,
133728
133745
  author: authorName,
133729
133746
  timestamp,
133730
- filesChanged
133747
+ filesChanged,
133748
+ signed
133731
133749
  };
133750
+ if (signingWarning) {
133751
+ result.signingWarning = signingWarning;
133752
+ }
133732
133753
  return result;
133733
133754
  } catch (error48) {
133734
133755
  throw mapGitError(error48, "commit");
@@ -134185,8 +134206,7 @@ async function executeMerge(options, context, execGit) {
134185
134206
  if (options.message) {
134186
134207
  args.push("-m", options.message);
134187
134208
  }
134188
- const shouldSign = options.sign ?? shouldSignCommits();
134189
- if (shouldSign) {
134209
+ if (shouldSignCommits()) {
134190
134210
  args.push("-S");
134191
134211
  }
134192
134212
  args.push(options.branch);
@@ -134283,8 +134303,7 @@ ${continueResult.stderr}`),
134283
134303
  if (options.preserve) {
134284
134304
  args.push("--preserve-merges");
134285
134305
  }
134286
- const shouldSign = options.sign ?? shouldSignCommits();
134287
- if (shouldSign) {
134306
+ if (shouldSignCommits()) {
134288
134307
  args.push("--gpg-sign");
134289
134308
  }
134290
134309
  if (options.onto) {
@@ -134366,8 +134385,7 @@ async function executeCherryPick(options, context, execGit) {
134366
134385
  if (options.signoff) {
134367
134386
  args.push("--signoff");
134368
134387
  }
134369
- const shouldSign = options.sign ?? shouldSignCommits();
134370
- if (shouldSign) {
134388
+ if (shouldSignCommits()) {
134371
134389
  args.push("--gpg-sign");
134372
134390
  }
134373
134391
  args.push(...options.commits);
@@ -134643,6 +134661,9 @@ async function executePull(options, context, execGit) {
134643
134661
  if (options.fastForwardOnly) {
134644
134662
  args.push("--ff-only");
134645
134663
  }
134664
+ if (shouldSignCommits()) {
134665
+ args.push("-S");
134666
+ }
134646
134667
  const cmd = buildGitCommand({ command: "pull", args });
134647
134668
  const result = await execGit(cmd, context.workingDirectory, context.requestContext, { allowNonZeroExit: true });
134648
134669
  const hasConflicts = result.stdout.includes("CONFLICT") || result.stderr.includes("CONFLICT");
@@ -134697,6 +134718,7 @@ function parseFilesChanged(stdout) {
134697
134718
  return files;
134698
134719
  }
134699
134720
  // src/services/git/providers/cli/operations/tags/tag.ts
134721
+ init_utils();
134700
134722
  async function executeTag(options, context, execGit) {
134701
134723
  try {
134702
134724
  const args = [];
@@ -134738,7 +134760,9 @@ async function executeTag(options, context, execGit) {
134738
134760
  if (!options.tagName) {
134739
134761
  throw new Error("Tag name is required for create operation");
134740
134762
  }
134741
- const shouldSign = options.sign ?? shouldSignCommits();
134763
+ const signRequested = shouldSignCommits();
134764
+ let signed = false;
134765
+ let signingWarning;
134742
134766
  const buildCreateArgs = (sign) => {
134743
134767
  const createArgs = [options.tagName];
134744
134768
  if (sign) {
@@ -134757,31 +134781,36 @@ async function executeTag(options, context, execGit) {
134757
134781
  }
134758
134782
  return createArgs;
134759
134783
  };
134760
- const configOverride = shouldSign ? [] : ["-c", "tag.gpgSign=false"];
134761
- const createCmd = [
134762
- ...configOverride,
134763
- ...buildGitCommand({
134764
- command: "tag",
134765
- args: buildCreateArgs(shouldSign)
134766
- })
134767
- ];
134784
+ const buildCmd = (sign) => {
134785
+ const configOverride = sign ? [] : ["-c", "tag.gpgSign=false"];
134786
+ return [
134787
+ ...configOverride,
134788
+ ...buildGitCommand({
134789
+ command: "tag",
134790
+ args: buildCreateArgs(sign)
134791
+ })
134792
+ ];
134793
+ };
134768
134794
  try {
134769
- await execGit(createCmd, context.workingDirectory, context.requestContext);
134795
+ await execGit(buildCmd(signRequested), context.workingDirectory, context.requestContext);
134796
+ signed = signRequested;
134770
134797
  } catch (error48) {
134771
- if (shouldSign && options.forceUnsignedOnFailure) {
134772
- const unsignedCmd = buildGitCommand({
134773
- command: "tag",
134774
- args: buildCreateArgs(false)
134775
- });
134776
- await execGit(unsignedCmd, context.workingDirectory, context.requestContext);
134777
- } else {
134798
+ if (!signRequested) {
134778
134799
  throw error48;
134779
134800
  }
134801
+ const errorMessage = error48 instanceof Error ? error48.message : String(error48);
134802
+ logger.warning("Tag signing failed; retrying unsigned. Set GIT_SIGN_COMMITS=false to suppress this attempt.", { ...context.requestContext, error: error48 });
134803
+ signingWarning = `GIT_SIGN_COMMITS is enabled but signing failed; tag created unsigned. Check signing key availability (gpg-agent running, SSH key accessible). Underlying error: ${errorMessage}`;
134804
+ await execGit(buildCmd(false), context.workingDirectory, context.requestContext);
134780
134805
  }
134781
134806
  const createResult = {
134782
134807
  mode: "create",
134783
- created: options.tagName
134808
+ created: options.tagName,
134809
+ signed
134784
134810
  };
134811
+ if (signingWarning) {
134812
+ createResult.signingWarning = signingWarning;
134813
+ }
134785
134814
  return createResult;
134786
134815
  }
134787
134816
  case "delete": {
@@ -145251,12 +145280,10 @@ var import_tsyringe7 = __toESM(require_cjs2(), 1);
145251
145280
  // src/mcp-server/prompts/definitions/git-wrapup.prompt.ts
145252
145281
  init_zod();
145253
145282
  var PROMPT_NAME = "git_wrapup";
145254
- var PROMPT_DESCRIPTION = "Generates a structured workflow prompt for wrapping up git sessions, including reviewing changes, updating documentation, and committing modifications.";
145283
+ var PROMPT_DESCRIPTION = "Orchestrates a full git wrap-up: loads the project-aware acceptance-criteria protocol from git_wrapup_instructions, analyzes changes, satisfies each criterion per project convention, commits atomically, and (optionally) tags the release.";
145255
145284
  var ArgumentsSchema = exports_external.object({
145256
- changelogPath: exports_external.string().optional().describe("Path to the changelog file to update (defaults to CHANGELOG.md)."),
145257
- skipDocumentation: exports_external.string().optional().describe("Whether to skip documentation review ('true' | 'false'). Defaults to 'false'."),
145258
- createTag: exports_external.string().optional().describe("Whether to create a git tag after committing ('true' | 'false'). Defaults to 'false'."),
145259
- updateAgentFiles: exports_external.string().optional().describe("Whether to update agent meta files like CLAUDE.md, AGENTS.md ('true' | 'false'). Defaults to 'false'.")
145285
+ changelogPath: exports_external.string().optional().describe("Path to the changelog file when the project uses a flat one (defaults to CHANGELOG.md). The protocol itself defers to project convention."),
145286
+ createTag: exports_external.string().optional().describe("Whether to include the tag criterion in the protocol ('true' | 'false'). Defaults to 'true' — set to 'false' when tagging is deferred to a separate release step.")
145260
145287
  });
145261
145288
  var gitWrapupPrompt = {
145262
145289
  name: PROMPT_NAME,
@@ -145264,57 +145291,34 @@ var gitWrapupPrompt = {
145264
145291
  argumentsSchema: ArgumentsSchema,
145265
145292
  generate: (args) => {
145266
145293
  const changelogPath = args.changelogPath || "CHANGELOG.md";
145267
- const skipDocumentation = args.skipDocumentation === "true";
145268
- const createTag = args.createTag === "true";
145269
- const updateAgentFiles = args.updateAgentFiles === "true";
145270
- const documentationSection = skipDocumentation ? "" : `
145271
- 4. **Review Documentation**: Read the README.md file and verify it accurately reflects the current codebase state. Update as necessary to maintain currency and accuracy.
145272
- `;
145273
- const agentFilesSection = updateAgentFiles ? `
145274
- 5. **Update Agent Files**: If present, review and update agent-specific meta files (CLAUDE.md, AGENTS.md, .clinerules/) to reflect any architectural or protocol changes.
145275
- ` : "";
145276
- const tagSection = createTag ? `
145277
- After all commits are complete and verified via git_status, create an annotated git tag using the git_tag tool. Use semantic versioning (e.g., v1.2.3) and include a summary of key changes in the annotation message.
145278
- ` : "";
145294
+ const includeTag = args.createTag !== "false";
145279
145295
  return [
145280
145296
  {
145281
145297
  role: "user",
145282
145298
  content: {
145283
145299
  type: "text",
145284
- text: `You are an expert git workflow manager. Execute a systematic wrap-up protocol for the current git session.
145300
+ text: `You are an expert git workflow manager. Run a complete wrap-up for the current git session.
145285
145301
 
145286
- ## Workflow Protocol
145302
+ ## Session Flow
145287
145303
 
145288
- Follow these steps in order. Do not proceed until the prior step is confirmed complete.
145304
+ 1. **Load Protocol**: Call \`git_wrapup_instructions\` with \`acknowledgement: "yes"\`${includeTag ? "" : " and `createTag: false`"}. It returns an acceptance-criteria checklist — every box must be satisfied before the wrap-up is complete — plus the current repository status.
145289
145305
 
145290
- 1. **Initialize Context**: First, call the \`git_wrapup_instructions\` tool with \`acknowledgement: "yes"\`${updateAgentFiles ? ' and `updateAgentMetaFiles: "yes"`' : ""}${createTag ? " and `createTag: true`" : ""}. This will provide the detailed workflow instructions and current repository status.
145306
+ 2. **Set Working Directory**: If not already set, call \`git_set_working_dir\` to establish the session context. Required before any git operations.
145291
145307
 
145292
- 2. **Set Working Directory**: If not already set, use \`git_set_working_dir\` to establish the session context. This is mandatory before any git operations.
145308
+ 3. **Analyze Changes**: Run \`git_diff\` with \`includeUntracked: true\`. Understand the "why" behind every modification end-to-end before grouping anything — the diff drives your commit plan and messages.
145293
145309
 
145294
- 3. **Analyze Changes**: Execute \`git_diff\` with \`includeUntracked: true\` to comprehensively understand all modifications. Analyze the diff output thoroughly to inform your commit strategy and messages.
145310
+ 4. **Satisfy the Acceptance Criteria**: Work each checkbox from step 1. The protocol is strict on outcomes, generic on mechanism — follow this project's own conventions for where versions live, how the changelog is formatted (default path when flat: \`${changelogPath}\`), and what the verification suite looks like. If the root agent-instruction file (\`AGENTS.md\`, \`CLAUDE.md\`, or equivalent) documents a project-specific wrap-up procedure, that takes precedence over the generic checklist.
145295
145311
 
145296
- 4. **Update Changelog**: Read the existing \`${changelogPath}\` file. Append a new version entry at the top that:
145297
- - Uses past tense and concise language
145298
- - Categorizes changes (Added, Changed, Fixed, Deprecated, Removed, Security)
145299
- - Follows the existing changelog format
145300
- - Provides enough detail for users to understand the impact
145301
- ${documentationSection}${agentFilesSection}
145302
- 5. **Commit Changes**: Use \`git_commit\` to create atomic, logical commits. For each commit:
145303
- - Group related changes together using the \`filesToStage\` parameter
145304
- - Write commit messages following Conventional Commits format (e.g., \`feat(auth): add password reset\`, \`fix(parser): handle edge case\`)
145305
- - Ensure commits are self-contained and buildable
145306
- - Do not mix unrelated changes in a single commit
145312
+ 5. **Commit Atomically**: Use \`git_commit\` to create logical, self-contained commits in Conventional Commits form. Group related changes with the \`filesToStage\` parameter. No mixing unrelated changes in one commit.
145307
145313
 
145308
- 6. **Verify Completion**: After all commits, run \`git_status\` to confirm the working directory is clean and all changes are committed.
145309
- ${tagSection}
145310
- ## Important Guidelines
145314
+ 6. **Verify Clean**: Run \`git_status\` to confirm the working tree is clean and every change is committed.${includeTag ? "\n\n7. **Tag the Release**: Create an annotated git tag with `git_tag` using semantic versioning (e.g., `v1.2.3`). The annotation message should summarize the real changes — no filler." : ""}
145311
145315
 
145312
- - **Do NOT push** to the remote repository unless explicitly instructed
145313
- - Create a task list before starting to track your progress
145314
- - Be thorough in your diff analysis - understand the "why" behind changes
145315
- - If you encounter merge conflicts or errors, stop and ask for guidance
145316
- - All commit messages must be clear, descriptive, and follow conventions
145317
- - Preserve existing code style and documentation formatting
145316
+ ## Constraints
145317
+
145318
+ - **Do not push** to the remote unless explicitly instructed.
145319
+ - Create a task list before starting so progress is trackable.
145320
+ - Do not bypass verification failures to land a green commit.
145321
+ - On merge conflicts or unexpected errors: stop and surface the blocker.
145318
145322
 
145319
145323
  Begin by calling \`git_wrapup_instructions\` and creating your task list.`
145320
145324
  }
@@ -145403,7 +145407,6 @@ var AllSchema = exports_external.boolean().default(false).describe("Include all
145403
145407
  var MergeStrategySchema = exports_external.enum(["ort", "recursive", "octopus", "ours", "subtree"]).optional().describe("Merge strategy to use (ort, recursive, octopus, ours, subtree).");
145404
145408
  var PruneSchema = exports_external.boolean().default(false).describe("Prune remote-tracking references that no longer exist on remote.");
145405
145409
  var DepthSchema = exports_external.number().int().min(1).optional().describe("Create a shallow clone with history truncated to N commits.");
145406
- var SignSchema = exports_external.boolean().optional().describe("Sign the commit/tag with GPG/SSH. Omit to use the server's GIT_SIGN_COMMITS default (recommended). Set true to force signing, or false to skip signing even when the default enables it.");
145407
145410
  var NoVerifySchema = exports_external.boolean().default(false).describe("Bypass pre-commit and commit-msg hooks.");
145408
145411
 
145409
145412
  // src/mcp-server/tools/utils/json-response-formatter.ts
@@ -146841,10 +146844,8 @@ var InputSchema12 = exports_external.object({
146841
146844
  }).optional().describe("Override commit author (defaults to git config)."),
146842
146845
  amend: exports_external.boolean().default(false).describe("Amend the previous commit instead of creating a new one. Use with caution."),
146843
146846
  allowEmpty: exports_external.boolean().default(false).describe("Allow creating a commit with no changes."),
146844
- sign: SignSchema,
146845
146847
  noVerify: NoVerifySchema,
146846
- filesToStage: exports_external.array(exports_external.string()).optional().describe("File paths to stage before committing (atomic stage+commit operation)."),
146847
- forceUnsignedOnFailure: exports_external.boolean().default(false).describe("If GPG/SSH signing fails, retry the commit without signing instead of failing.")
146848
+ filesToStage: exports_external.array(exports_external.string()).optional().describe("File paths to stage before committing (atomic stage+commit operation).")
146848
146849
  });
146849
146850
  var OutputSchema13 = exports_external.object({
146850
146851
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
@@ -146856,6 +146857,8 @@ var OutputSchema13 = exports_external.object({
146856
146857
  committedFiles: exports_external.array(exports_external.string()).describe("List of files that were committed."),
146857
146858
  insertions: exports_external.number().int().optional().describe("Number of line insertions."),
146858
146859
  deletions: exports_external.number().int().optional().describe("Number of line deletions."),
146860
+ signed: exports_external.boolean().describe("Whether the commit was signed. False when GIT_SIGN_COMMITS=false or when signing was attempted and fell back to unsigned on failure."),
146861
+ signingWarning: exports_external.string().optional().describe("Populated only when signing was requested but failed, and the commit was created unsigned as a fallback."),
146859
146862
  status: exports_external.object({
146860
146863
  current_branch: exports_external.string().nullable().describe("Current branch name after commit."),
146861
146864
  staged_changes: exports_external.record(exports_external.string(), exports_external.any()).describe("Remaining staged changes after commit."),
@@ -146877,15 +146880,11 @@ async function gitCommitLogic(input, { provider, targetPath, appContext }) {
146877
146880
  message: input.message,
146878
146881
  amend: input.amend,
146879
146882
  allowEmpty: input.allowEmpty,
146880
- noVerify: input.noVerify,
146881
- forceUnsignedOnFailure: input.forceUnsignedOnFailure
146883
+ noVerify: input.noVerify
146882
146884
  };
146883
146885
  if (input.author !== undefined) {
146884
146886
  commitOptions.author = input.author;
146885
146887
  }
146886
- if (input.sign !== undefined) {
146887
- commitOptions.sign = input.sign;
146888
- }
146889
146888
  const result = await provider.commit(commitOptions, {
146890
146889
  workingDirectory: targetPath,
146891
146890
  requestContext: appContext,
@@ -146896,7 +146895,7 @@ async function gitCommitLogic(input, { provider, targetPath, appContext }) {
146896
146895
  requestContext: appContext,
146897
146896
  tenantId: appContext.tenantId || "default-tenant"
146898
146897
  });
146899
- return {
146898
+ const output = {
146900
146899
  success: result.success,
146901
146900
  commitHash: result.commitHash,
146902
146901
  message: result.message,
@@ -146904,6 +146903,7 @@ async function gitCommitLogic(input, { provider, targetPath, appContext }) {
146904
146903
  timestamp: result.timestamp,
146905
146904
  filesChanged: result.filesChanged.length,
146906
146905
  committedFiles: result.filesChanged,
146906
+ signed: result.signed,
146907
146907
  status: {
146908
146908
  current_branch: statusResult.currentBranch,
146909
146909
  staged_changes: flattenChanges(statusResult.stagedChanges),
@@ -146913,6 +146913,10 @@ async function gitCommitLogic(input, { provider, targetPath, appContext }) {
146913
146913
  is_clean: statusResult.isClean
146914
146914
  }
146915
146915
  };
146916
+ if (result.signingWarning) {
146917
+ output.signingWarning = result.signingWarning;
146918
+ }
146919
+ return output;
146916
146920
  }
146917
146921
  function filterGitCommitOutput(result, level) {
146918
146922
  if (level === "minimal") {
@@ -146920,6 +146924,8 @@ function filterGitCommitOutput(result, level) {
146920
146924
  success: result.success,
146921
146925
  commitHash: result.commitHash,
146922
146926
  message: result.message,
146927
+ signed: result.signed,
146928
+ ...result.signingWarning && { signingWarning: result.signingWarning },
146923
146929
  status: {
146924
146930
  current_branch: result.status.current_branch,
146925
146931
  is_clean: result.status.is_clean,
@@ -146941,6 +146947,8 @@ function filterGitCommitOutput(result, level) {
146941
146947
  insertions: result.insertions,
146942
146948
  deletions: result.deletions,
146943
146949
  committedFiles: result.committedFiles,
146950
+ signed: result.signed,
146951
+ ...result.signingWarning && { signingWarning: result.signingWarning },
146944
146952
  status: {
146945
146953
  current_branch: result.status.current_branch,
146946
146954
  is_clean: result.status.is_clean,
@@ -148189,9 +148197,7 @@ var InputSchema27 = exports_external.object({
148189
148197
  tagName: TagNameSchema.optional().describe("Tag name for create/delete operations."),
148190
148198
  commit: CommitRefSchema.optional().describe("Commit to tag (default: HEAD for create operation)."),
148191
148199
  message: exports_external.string().optional().describe("Tag message. Providing a message always produces an annotated tag (git does not support messages on lightweight tags). For release tags, summarize notable changes."),
148192
- annotated: exports_external.boolean().default(false).describe('Create an annotated tag with a default "Tag <name>" message. Only effective when both message and sign are absent — otherwise the tag is always annotated.'),
148193
- sign: SignSchema,
148194
- forceUnsignedOnFailure: exports_external.boolean().default(false).describe("If GPG/SSH signing fails, retry the tag creation without signing instead of failing."),
148200
+ annotated: exports_external.boolean().default(false).describe('Create an annotated tag with a default "Tag <name>" message. Only effective when no message is provided and signing is disabled — otherwise the tag is always annotated.'),
148195
148201
  force: ForceSchema.describe("Overwrite an existing tag (create mode only; has no effect on list or delete).")
148196
148202
  });
148197
148203
  var TagInfoSchema = exports_external.object({
@@ -148206,7 +148212,9 @@ var OutputSchema28 = exports_external.object({
148206
148212
  mode: exports_external.string().describe("Operation mode that was performed."),
148207
148213
  tags: exports_external.array(TagInfoSchema).optional().describe("List of tags (for list mode)."),
148208
148214
  created: exports_external.string().optional().describe("Created tag name (for create mode)."),
148209
- deleted: exports_external.string().optional().describe("Deleted tag name (for delete mode).")
148215
+ deleted: exports_external.string().optional().describe("Deleted tag name (for delete mode)."),
148216
+ signed: exports_external.boolean().optional().describe("Whether the created tag was signed. Only populated for create mode. False when GIT_SIGN_COMMITS=false or when signing failed and fell back to unsigned."),
148217
+ signingWarning: exports_external.string().optional().describe("Populated only when signing was requested but failed, and the tag was created unsigned as a fallback.")
148210
148218
  });
148211
148219
  async function gitTagLogic(input, { provider, targetPath, appContext }) {
148212
148220
  if ((input.mode === "create" || input.mode === "delete") && !input.tagName) {
@@ -148215,8 +148223,7 @@ async function gitTagLogic(input, { provider, targetPath, appContext }) {
148215
148223
  const tagOptions = {
148216
148224
  mode: input.mode,
148217
148225
  annotated: input.annotated,
148218
- force: input.force,
148219
- forceUnsignedOnFailure: input.forceUnsignedOnFailure
148226
+ force: input.force
148220
148227
  };
148221
148228
  if (input.tagName !== undefined) {
148222
148229
  tagOptions.tagName = input.tagName;
@@ -148227,27 +148234,33 @@ async function gitTagLogic(input, { provider, targetPath, appContext }) {
148227
148234
  if (input.message !== undefined) {
148228
148235
  tagOptions.message = normalizeMessage(input.message);
148229
148236
  }
148230
- if (input.sign !== undefined) {
148231
- tagOptions.sign = input.sign;
148232
- }
148233
148237
  const result = await provider.tag(tagOptions, {
148234
148238
  workingDirectory: targetPath,
148235
148239
  requestContext: appContext,
148236
148240
  tenantId: appContext.tenantId || "default-tenant"
148237
148241
  });
148238
- return {
148242
+ const output = {
148239
148243
  success: true,
148240
148244
  mode: result.mode,
148241
148245
  tags: result.tags,
148242
148246
  created: result.created,
148243
148247
  deleted: result.deleted
148244
148248
  };
148249
+ if (result.signed !== undefined) {
148250
+ output.signed = result.signed;
148251
+ }
148252
+ if (result.signingWarning) {
148253
+ output.signingWarning = result.signingWarning;
148254
+ }
148255
+ return output;
148245
148256
  }
148246
148257
  function filterGitTagOutput(result, level) {
148247
148258
  if (level === "minimal") {
148248
148259
  return {
148249
148260
  success: result.success,
148250
- mode: result.mode
148261
+ mode: result.mode,
148262
+ ...result.signed !== undefined && { signed: result.signed },
148263
+ ...result.signingWarning && { signingWarning: result.signingWarning }
148251
148264
  };
148252
148265
  }
148253
148266
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/git-mcp-server",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
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",