@cyanheads/git-mcp-server 2.4.8 → 2.5.1

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 +8 -5
  2. package/dist/index.js +381 -42
  3. package/package.json +29 -31
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.4.8-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--06--18-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-06-18/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.20.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.4.9-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--06--18-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-06-18/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.20.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
 
@@ -73,7 +73,7 @@ Add the following to your MCP Client configuration file (e.g., `cline_mcp_settin
73
73
  "env": {
74
74
  "MCP_TRANSPORT_TYPE": "stdio",
75
75
  "MCP_LOG_LEVEL": "info",
76
- "GIT_MCP_BASE_DIR": "~/Developer/",
76
+ "GIT_BASE_DIR": "~/Developer/",
77
77
  "LOGS_DIR": "~/Developer/logs/git-mcp-server/",
78
78
  "GIT_USERNAME": "cyanheads",
79
79
  "GIT_EMAIL": "casey@caseyjhand.com",
@@ -96,7 +96,7 @@ Add the following to your MCP Client configuration file (e.g., `cline_mcp_settin
96
96
  "env": {
97
97
  "MCP_TRANSPORT_TYPE": "stdio",
98
98
  "MCP_LOG_LEVEL": "info",
99
- "GIT_MCP_BASE_DIR": "~/Developer/",
99
+ "GIT_BASE_DIR": "~/Developer/",
100
100
  "LOGS_DIR": "~/Developer/logs/git-mcp-server/",
101
101
  "GIT_USERNAME": "cyanheads",
102
102
  "GIT_EMAIL": "casey@caseyjhand.com",
@@ -133,8 +133,9 @@ Plus, specialized features for **Git integration**:
133
133
  - **Optimized Git Execution**: Direct git CLI interaction with cross-runtime support for high-performance process management, streaming I/O, and timeout handling (current CLI provider).
134
134
  - **Comprehensive Coverage**: 27 tools covering all essential Git operations from init to push.
135
135
  - **Working Directory Management**: Session-specific directory context for multi-repo workflows.
136
+ - **Configurable Git Identity**: Override author/committer information via environment variables with automatic fallback to global git config.
136
137
  - **Safety Features**: Explicit confirmations for destructive operations like `git clean` and `git reset --hard`.
137
- - **Commit Signing**: Optional GPG/SSH signing support for verified commits.
138
+ - **Commit Signing**: Optional GPG/SSH signing support for all commit-creating operations (commits, merges, rebases, cherry-picks, and tags).
138
139
 
139
140
  ### Development Environment Setup
140
141
 
@@ -190,7 +191,9 @@ All configuration is centralized and validated at startup in `src/config/index.t
190
191
  | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv`, `r2`. | `in-memory` |
191
192
  | `OTEL_ENABLED` | Set to `true` to enable OpenTelemetry. | `false` |
192
193
  | `MCP_LOG_LEVEL` | The minimum level for logging (`debug`, `info`, `warn`, `error`). | `info` |
193
- | `GIT_SIGN_COMMITS` | Set to `"true"` to enable GPG/SSH signing for commits. Requires server-side Git configuration. | `false` |
194
+ | `GIT_SIGN_COMMITS` | Set to `"true"` to enable GPG/SSH signing for all commits, merges, rebases, cherry-picks, and tags. Requires GPG/SSH configuration. | `false` |
195
+ | `GIT_AUTHOR_NAME` | Git author name. Aliases: `GIT_USERNAME`, `GIT_USER`. Falls back to global git config if not set. | `(none)` |
196
+ | `GIT_AUTHOR_EMAIL` | Git author email. Aliases: `GIT_EMAIL`, `GIT_USER_EMAIL`. Falls back to global git config if not set. | `(none)` |
194
197
  | `GIT_BASE_DIR` | Optional absolute path to restrict all git operations to a specific directory tree. Provides security sandboxing for multi-tenant or shared environments. | `(none)` |
195
198
  | `GIT_WRAPUP_INSTRUCTIONS_PATH` | Optional path to custom markdown file with Git workflow instructions. | `(none)` |
196
199
  | `MCP_AUTH_SECRET_KEY` | **Required for `jwt` auth.** A 32+ character secret key. | `(none)` |
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ var __create = Object.create;
4
4
  var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
9
  var __toESM = (mod2, isNodeMode, target) => {
9
10
  target = mod2 != null ? __create(__getProtoOf(mod2)) : {};
@@ -16,6 +17,20 @@ var __toESM = (mod2, isNodeMode, target) => {
16
17
  });
17
18
  return to;
18
19
  };
20
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
21
+ var __toCommonJS = (from) => {
22
+ var entry = __moduleCache.get(from), desc;
23
+ if (entry)
24
+ return entry;
25
+ entry = __defProp({}, "__esModule", { value: true });
26
+ if (from && typeof from === "object" || typeof from === "function")
27
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
+ get: () => from[key],
29
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
+ }));
31
+ __moduleCache.set(from, entry);
32
+ return entry;
33
+ };
19
34
  var __commonJS = (cb, mod2) => () => (mod2 || cb((mod2 = { exports: {} }).exports, mod2), mod2.exports);
20
35
  var __export = (target, all) => {
21
36
  for (var name in all)
@@ -4246,7 +4261,7 @@ var package_default;
4246
4261
  var init_package = __esm(() => {
4247
4262
  package_default = {
4248
4263
  name: "@cyanheads/git-mcp-server",
4249
- version: "2.4.7",
4264
+ version: "2.5.1",
4250
4265
  mcpName: "io.github.cyanheads/git-mcp-server",
4251
4266
  description: "A secure and scalable Git MCP server enabling AI agents to perform comprehensive Git version control operations via STDIO and Streamable HTTP.",
4252
4267
  main: "dist/index.js",
@@ -4312,33 +4327,12 @@ var init_package = __esm(() => {
4312
4327
  zod: "3.23.8",
4313
4328
  typescript: "5.9.3"
4314
4329
  },
4315
- dependencies: {
4330
+ devDependencies: {
4331
+ "@cloudflare/workers-types": "^4.20251011.0",
4332
+ "@eslint/js": "^9.37.0",
4316
4333
  "@hono/mcp": "^0.1.4",
4317
4334
  "@hono/node-server": "^1.19.5",
4318
4335
  "@modelcontextprotocol/sdk": "^1.20.0",
4319
- "@supabase/supabase-js": "^2.75.0",
4320
- axios: "^1.12.2",
4321
- "chrono-node": "^2.9.0",
4322
- dotenv: "^17.2.3",
4323
- "fast-xml-parser": "^5.3.0",
4324
- hono: "^4.9.12",
4325
- ignore: "^7.0.5",
4326
- jose: "^6.1.0",
4327
- "js-yaml": "^4.1.0",
4328
- "node-cron": "^4.2.1",
4329
- openai: "^6.3.0",
4330
- papaparse: "^5.5.3",
4331
- "partial-json": "^0.1.7",
4332
- "pdf-lib": "^1.17.1",
4333
- pino: "^10.0.0",
4334
- "pino-pretty": "^13.1.2",
4335
- "reflect-metadata": "^0.2.2",
4336
- repomix: "^1.7.0",
4337
- "sanitize-html": "^2.17.0",
4338
- tslib: "^2.8.1",
4339
- tsyringe: "^4.10.0",
4340
- validator: "13.15.15",
4341
- zod: "^3.23.8",
4342
4336
  "@opentelemetry/api": "^1.9.0",
4343
4337
  "@opentelemetry/auto-instrumentations-node": "^0.65.0",
4344
4338
  "@opentelemetry/exporter-metrics-otlp-http": "^0.206.0",
@@ -4348,11 +4342,8 @@ var init_package = __esm(() => {
4348
4342
  "@opentelemetry/sdk-metrics": "^2.1.0",
4349
4343
  "@opentelemetry/sdk-node": "^0.206.0",
4350
4344
  "@opentelemetry/sdk-trace-node": "^2.1.0",
4351
- "@opentelemetry/semantic-conventions": "^1.37.0"
4352
- },
4353
- devDependencies: {
4354
- "@cloudflare/workers-types": "^4.20251011.0",
4355
- "@eslint/js": "^9.37.0",
4345
+ "@opentelemetry/semantic-conventions": "^1.37.0",
4346
+ "@supabase/supabase-js": "^2.75.0",
4356
4347
  "@types/bun": "^1.3.0",
4357
4348
  "@types/js-yaml": "^4.0.9",
4358
4349
  "@types/node": "^24.7.2",
@@ -4363,21 +4354,43 @@ var init_package = __esm(() => {
4363
4354
  "@vitest/coverage-v8": "3.2.4",
4364
4355
  ajv: "^8.17.1",
4365
4356
  "ajv-formats": "^3.0.1",
4357
+ axios: "^1.12.2",
4366
4358
  "bun-types": "^1.3.0",
4359
+ "chrono-node": "^2.9.0",
4367
4360
  clipboardy: "^5.0.0",
4368
4361
  depcheck: "^1.4.7",
4362
+ dotenv: "^17.2.3",
4369
4363
  eslint: "^9.37.0",
4370
4364
  execa: "^9.6.0",
4365
+ "fast-xml-parser": "^5.3.0",
4371
4366
  globals: "^16.4.0",
4367
+ hono: "^4.9.12",
4372
4368
  husky: "^9.1.7",
4369
+ ignore: "^7.0.5",
4370
+ jose: "^6.1.0",
4371
+ "js-yaml": "^4.1.0",
4373
4372
  msw: "^2.11.5",
4373
+ "node-cron": "^4.2.1",
4374
+ openai: "^6.3.0",
4375
+ papaparse: "^5.5.3",
4376
+ "partial-json": "^0.1.7",
4377
+ "pdf-lib": "^1.17.1",
4378
+ pino: "^10.0.0",
4379
+ "pino-pretty": "^13.1.2",
4374
4380
  prettier: "^3.6.2",
4381
+ "reflect-metadata": "^0.2.2",
4382
+ repomix: "^1.7.0",
4383
+ "sanitize-html": "^2.17.0",
4384
+ tslib: "^2.8.1",
4385
+ tsyringe: "^4.10.0",
4375
4386
  typedoc: "^0.28.14",
4376
4387
  typescript: "^5.9.3",
4377
4388
  "typescript-eslint": "8.46.0",
4389
+ validator: "13.15.15",
4378
4390
  vite: "7.1.9",
4379
4391
  "vite-tsconfig-paths": "^5.1.4",
4380
- vitest: "^3.2.4"
4392
+ vitest: "^3.2.4",
4393
+ zod: "^3.23.8"
4381
4394
  },
4382
4395
  keywords: [
4383
4396
  "ai-agent",
@@ -4574,6 +4587,10 @@ var import_dotenv, packageManifest, hasFileSystemAccess, emptyStringAsUndefined
4574
4587
  git: {
4575
4588
  provider: env.GIT_PROVIDER,
4576
4589
  signCommits: env.GIT_SIGN_COMMITS,
4590
+ authorName: env.GIT_AUTHOR_NAME || env.GIT_USERNAME || env.GIT_USER || undefined,
4591
+ authorEmail: env.GIT_AUTHOR_EMAIL || env.GIT_EMAIL || env.GIT_USER_EMAIL || undefined,
4592
+ committerName: env.GIT_COMMITTER_NAME || env.GIT_USERNAME || env.GIT_USER || undefined,
4593
+ committerEmail: env.GIT_COMMITTER_EMAIL || env.GIT_EMAIL || env.GIT_USER_EMAIL || undefined,
4577
4594
  wrapupInstructionsPath: env.GIT_WRAPUP_INSTRUCTIONS_PATH,
4578
4595
  baseDir: env.GIT_BASE_DIR,
4579
4596
  maxCommandTimeoutMs: env.GIT_MAX_COMMAND_TIMEOUT_MS,
@@ -4753,6 +4770,10 @@ var init_config = __esm(() => {
4753
4770
  git: z.object({
4754
4771
  provider: z.preprocess(emptyStringAsUndefined, z.enum(["auto", "cli", "isomorphic"]).default("auto")),
4755
4772
  signCommits: z.coerce.boolean().default(false),
4773
+ authorName: z.string().optional(),
4774
+ authorEmail: z.string().email().optional(),
4775
+ committerName: z.string().optional(),
4776
+ committerEmail: z.string().email().optional(),
4756
4777
  wrapupInstructionsPath: z.preprocess(expandTildePath, z.string().optional()),
4757
4778
  baseDir: z.preprocess((val) => expandTildePath(emptyStringAsUndefined(val)), z.string().refine((path) => !path || path.startsWith("/"), {
4758
4779
  message: 'GIT_BASE_DIR must be an absolute path starting with "/" (tilde expansion is supported)'
@@ -13784,7 +13805,7 @@ var require_propwrap = __commonJS((exports) => {
13784
13805
  Object.defineProperty(exports, "__esModule", { value: true });
13785
13806
  exports.propwrap = undefined;
13786
13807
  var __defProp2 = Object.defineProperty;
13787
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13808
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
13788
13809
  var __hasOwnProp2 = Object.prototype.hasOwnProperty;
13789
13810
  var __getOwnPropNames2 = Object.getOwnPropertyNames;
13790
13811
  var __copyProps = (to, from, except, desc) => {
@@ -13793,7 +13814,7 @@ var require_propwrap = __commonJS((exports) => {
13793
13814
  if (!__hasOwnProp2.call(to, key) && key !== except) {
13794
13815
  __defProp2(to, key, {
13795
13816
  get: () => from[key],
13796
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
13817
+ enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable
13797
13818
  });
13798
13819
  }
13799
13820
  }
@@ -13828,7 +13849,7 @@ var require_propwrap = __commonJS((exports) => {
13828
13849
  } else {
13829
13850
  val = namespaces[i + 1];
13830
13851
  }
13831
- const desc = __getOwnPropDesc(namespace, key);
13852
+ const desc = __getOwnPropDesc2(namespace, key);
13832
13853
  const wrappedNamespace = __defProp2({}, key, {
13833
13854
  value: val,
13834
13855
  enumerable: !desc || desc.enumerable
@@ -151663,6 +151684,14 @@ function buildGitCommand(config2) {
151663
151684
  }
151664
151685
  return parts;
151665
151686
  }
151687
+ function loadConfig() {
151688
+ try {
151689
+ const configModule = (init_config(), __toCommonJS(exports_config));
151690
+ return configModule.config;
151691
+ } catch {
151692
+ return null;
151693
+ }
151694
+ }
151666
151695
  function buildGitEnv(additionalEnv) {
151667
151696
  const env = { ...process.env };
151668
151697
  Object.assign(env, {
@@ -151670,6 +151699,21 @@ function buildGitEnv(additionalEnv) {
151670
151699
  LANG: "en_US.UTF-8",
151671
151700
  LC_ALL: "en_US.UTF-8"
151672
151701
  });
151702
+ const config2 = loadConfig();
151703
+ if (config2?.git) {
151704
+ if (config2.git.authorName) {
151705
+ env.GIT_AUTHOR_NAME = config2.git.authorName;
151706
+ }
151707
+ if (config2.git.authorEmail) {
151708
+ env.GIT_AUTHOR_EMAIL = config2.git.authorEmail;
151709
+ }
151710
+ if (config2.git.committerName) {
151711
+ env.GIT_COMMITTER_NAME = config2.git.committerName;
151712
+ }
151713
+ if (config2.git.committerEmail) {
151714
+ env.GIT_COMMITTER_EMAIL = config2.git.committerEmail;
151715
+ }
151716
+ }
151673
151717
  if (additionalEnv) {
151674
151718
  Object.assign(env, additionalEnv);
151675
151719
  }
@@ -151732,6 +151776,20 @@ function validateGitArgs(args) {
151732
151776
  }
151733
151777
  }
151734
151778
 
151779
+ // src/services/git/providers/cli/utils/config-helper.ts
151780
+ function loadConfig2() {
151781
+ try {
151782
+ const configModule = (init_config(), __toCommonJS(exports_config));
151783
+ return configModule.config;
151784
+ } catch {
151785
+ return null;
151786
+ }
151787
+ }
151788
+ function shouldSignCommits() {
151789
+ const config2 = loadConfig2();
151790
+ return config2?.git?.signCommits ?? false;
151791
+ }
151792
+
151735
151793
  // src/services/git/providers/cli/utils/error-mapper.ts
151736
151794
  init_errors();
151737
151795
  var ERROR_PATTERNS = [
@@ -151871,13 +151929,24 @@ function detectRuntime2() {
151871
151929
  }
151872
151930
  return "node";
151873
151931
  }
151874
- async function spawnWithBun(args, cwd, env, timeout) {
151932
+ async function spawnWithBun(args, cwd, env, timeout, signal) {
151875
151933
  const bunApi = globalThis.Bun;
151934
+ if (signal?.aborted) {
151935
+ throw new Error(`Git command cancelled before execution: git ${args.join(" ")}`);
151936
+ }
151876
151937
  const proc = bunApi.spawn(["git", ...args], {
151877
151938
  cwd,
151878
151939
  env,
151879
151940
  stdio: ["ignore", "pipe", "pipe"]
151880
151941
  });
151942
+ const abortPromise = new Promise((_, reject) => {
151943
+ if (signal) {
151944
+ signal.addEventListener("abort", () => {
151945
+ proc.kill();
151946
+ reject(new Error(`Git command cancelled: git ${args.join(" ")}`));
151947
+ }, { once: true });
151948
+ }
151949
+ });
151881
151950
  const timeoutPromise = new Promise((_, reject) => {
151882
151951
  const timeoutId = setTimeout(() => {
151883
151952
  proc.kill();
@@ -151885,7 +151954,11 @@ async function spawnWithBun(args, cwd, env, timeout) {
151885
151954
  }, timeout);
151886
151955
  proc.exited.finally(() => clearTimeout(timeoutId));
151887
151956
  });
151888
- const exitCode = await Promise.race([proc.exited, timeoutPromise]);
151957
+ const exitCode = await Promise.race([
151958
+ proc.exited,
151959
+ timeoutPromise,
151960
+ ...signal ? [abortPromise] : []
151961
+ ]);
151889
151962
  const [stdout, stderr] = await Promise.all([
151890
151963
  proc.stdout.text(),
151891
151964
  proc.stderr.text()
@@ -151898,8 +151971,12 @@ Stdout: ${stdout}`;
151898
151971
  }
151899
151972
  return { stdout, stderr };
151900
151973
  }
151901
- async function spawnWithNode(args, cwd, env, timeout) {
151974
+ async function spawnWithNode(args, cwd, env, timeout, signal) {
151902
151975
  return new Promise((resolve, reject) => {
151976
+ if (signal?.aborted) {
151977
+ reject(new Error(`Git command cancelled before execution: ${args.join(" ")}`));
151978
+ return;
151979
+ }
151903
151980
  const proc = spawn("git", args, {
151904
151981
  cwd,
151905
151982
  env,
@@ -151913,16 +151990,29 @@ async function spawnWithNode(args, cwd, env, timeout) {
151913
151990
  proc.stderr.on("data", (chunk) => {
151914
151991
  stderrChunks.push(chunk);
151915
151992
  });
151993
+ const abortHandler = () => {
151994
+ proc.kill("SIGTERM");
151995
+ reject(new Error(`Git command cancelled: ${args.join(" ")}`));
151996
+ };
151997
+ if (signal) {
151998
+ signal.addEventListener("abort", abortHandler, { once: true });
151999
+ }
151916
152000
  const timeoutHandle = setTimeout(() => {
151917
152001
  proc.kill("SIGTERM");
151918
152002
  reject(new Error(`Git command timed out after ${timeout / 1000}s: ${args.join(" ")}`));
151919
152003
  }, timeout);
151920
152004
  proc.on("error", (error) => {
151921
152005
  clearTimeout(timeoutHandle);
152006
+ if (signal) {
152007
+ signal.removeEventListener("abort", abortHandler);
152008
+ }
151922
152009
  reject(error);
151923
152010
  });
151924
152011
  proc.on("close", (exitCode) => {
151925
152012
  clearTimeout(timeoutHandle);
152013
+ if (signal) {
152014
+ signal.removeEventListener("abort", abortHandler);
152015
+ }
151926
152016
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
151927
152017
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
151928
152018
  if (exitCode !== 0) {
@@ -151936,12 +152026,12 @@ Stdout: ${stdout}`;
151936
152026
  });
151937
152027
  });
151938
152028
  }
151939
- async function spawnGitCommand(args, cwd, env, timeout = 60000) {
152029
+ async function spawnGitCommand(args, cwd, env, timeout = 60000, signal) {
151940
152030
  const runtime2 = detectRuntime2();
151941
152031
  if (runtime2 === "bun") {
151942
- return spawnWithBun(args, cwd, env, timeout);
152032
+ return spawnWithBun(args, cwd, env, timeout, signal);
151943
152033
  } else {
151944
- return spawnWithNode(args, cwd, env, timeout);
152034
+ return spawnWithNode(args, cwd, env, timeout, signal);
151945
152035
  }
151946
152036
  }
151947
152037
 
@@ -152327,7 +152417,8 @@ async function executeCommit(options, context, execGit) {
152327
152417
  if (options.noVerify) {
152328
152418
  args.push("--no-verify");
152329
152419
  }
152330
- if (options.sign) {
152420
+ const shouldSign = options.sign ?? shouldSignCommits();
152421
+ if (shouldSign) {
152331
152422
  args.push("--gpg-sign");
152332
152423
  }
152333
152424
  if (options.author) {
@@ -152627,6 +152718,10 @@ async function executeMerge(options, context, execGit) {
152627
152718
  if (options.message) {
152628
152719
  args.push("-m", options.message);
152629
152720
  }
152721
+ const shouldSign = options.sign ?? shouldSignCommits();
152722
+ if (shouldSign) {
152723
+ args.push("-S");
152724
+ }
152630
152725
  const cmd = buildGitCommand({ command: "merge", args });
152631
152726
  const result = await execGit(cmd, context.workingDirectory, context.requestContext);
152632
152727
  const hasConflicts = result.stdout.includes("CONFLICT") || result.stderr.includes("CONFLICT");
@@ -152706,6 +152801,10 @@ async function executeRebase(options, context, execGit) {
152706
152801
  if (options.preserve) {
152707
152802
  args.push("--preserve-merges");
152708
152803
  }
152804
+ const shouldSign = options.sign ?? shouldSignCommits();
152805
+ if (shouldSign) {
152806
+ args.push("--gpg-sign");
152807
+ }
152709
152808
  }
152710
152809
  const cmd = buildGitCommand({ command: "rebase", args });
152711
152810
  const result = await execGit(cmd, context.workingDirectory, context.requestContext);
@@ -152741,6 +152840,10 @@ async function executeCherryPick(options, context, execGit) {
152741
152840
  if (options.noCommit) {
152742
152841
  args.push("--no-commit");
152743
152842
  }
152843
+ const shouldSign = options.sign ?? shouldSignCommits();
152844
+ if (shouldSign) {
152845
+ args.push("--gpg-sign");
152846
+ }
152744
152847
  }
152745
152848
  const cmd = buildGitCommand({ command: "cherry-pick", args });
152746
152849
  const result = await execGit(cmd, context.workingDirectory, context.requestContext);
@@ -153041,7 +153144,11 @@ async function executeTag(options, context, execGit) {
153041
153144
  throw new Error("Tag name is required for create operation");
153042
153145
  }
153043
153146
  args.push(options.tagName);
153044
- if (options.message && options.annotated) {
153147
+ const shouldSign = options.sign ?? shouldSignCommits();
153148
+ if (shouldSign) {
153149
+ const message = options.message || `Tag ${options.tagName}`;
153150
+ args.push("-s", "-m", message);
153151
+ } else if (options.message && options.annotated) {
153045
153152
  args.push("-a", "-m", options.message);
153046
153153
  }
153047
153154
  if (options.commit) {
@@ -172049,6 +172156,163 @@ var httpErrorHandler = async (err, c) => {
172049
172156
  return c.json(errorResponse);
172050
172157
  };
172051
172158
 
172159
+ // src/mcp-server/transports/http/sessionManager.ts
172160
+ init_utils();
172161
+
172162
+ class SessionManager {
172163
+ static instance = null;
172164
+ sessions = new Map;
172165
+ cleanupIntervalId = null;
172166
+ staleTimeoutMs;
172167
+ cleanupIntervalMs;
172168
+ constructor(staleTimeoutMs = 30 * 60 * 1000, cleanupIntervalMs = 5 * 60 * 1000) {
172169
+ this.staleTimeoutMs = staleTimeoutMs;
172170
+ this.cleanupIntervalMs = cleanupIntervalMs;
172171
+ this.startCleanupInterval();
172172
+ }
172173
+ static getInstance(staleTimeoutMs, cleanupIntervalMs) {
172174
+ if (!SessionManager.instance) {
172175
+ SessionManager.instance = new SessionManager(staleTimeoutMs, cleanupIntervalMs);
172176
+ }
172177
+ return SessionManager.instance;
172178
+ }
172179
+ static resetInstance() {
172180
+ if (SessionManager.instance) {
172181
+ SessionManager.instance.stopCleanupInterval();
172182
+ SessionManager.instance = null;
172183
+ }
172184
+ }
172185
+ createSession(sessionId, clientId, tenantId) {
172186
+ const now2 = Date.now();
172187
+ const metadata = {
172188
+ sessionId,
172189
+ createdAt: now2,
172190
+ lastActivityAt: now2,
172191
+ ...clientId !== undefined && { clientId },
172192
+ ...tenantId !== undefined && { tenantId }
172193
+ };
172194
+ this.sessions.set(sessionId, metadata);
172195
+ logger.debug("Created new MCP session", {
172196
+ ...requestContextService.createRequestContext({
172197
+ operation: "SessionManager.createSession"
172198
+ }),
172199
+ sessionId,
172200
+ ...clientId !== undefined && { clientId },
172201
+ ...tenantId !== undefined && { tenantId },
172202
+ totalSessions: this.sessions.size
172203
+ });
172204
+ return sessionId;
172205
+ }
172206
+ isSessionValid(sessionId) {
172207
+ const session = this.sessions.get(sessionId);
172208
+ if (!session) {
172209
+ return false;
172210
+ }
172211
+ const now2 = Date.now();
172212
+ const age = now2 - session.lastActivityAt;
172213
+ if (age > this.staleTimeoutMs) {
172214
+ logger.info("Session expired due to inactivity", {
172215
+ ...requestContextService.createRequestContext({
172216
+ operation: "SessionManager.isSessionValid"
172217
+ }),
172218
+ sessionId,
172219
+ ageMs: age,
172220
+ staleTimeoutMs: this.staleTimeoutMs
172221
+ });
172222
+ this.sessions.delete(sessionId);
172223
+ return false;
172224
+ }
172225
+ return true;
172226
+ }
172227
+ touchSession(sessionId) {
172228
+ const session = this.sessions.get(sessionId);
172229
+ if (session) {
172230
+ session.lastActivityAt = Date.now();
172231
+ }
172232
+ }
172233
+ terminateSession(sessionId) {
172234
+ const existed = this.sessions.has(sessionId);
172235
+ this.sessions.delete(sessionId);
172236
+ if (existed) {
172237
+ logger.info("Session explicitly terminated", {
172238
+ ...requestContextService.createRequestContext({
172239
+ operation: "SessionManager.terminateSession"
172240
+ }),
172241
+ sessionId,
172242
+ remainingSessions: this.sessions.size
172243
+ });
172244
+ }
172245
+ return existed;
172246
+ }
172247
+ getSessionMetadata(sessionId) {
172248
+ if (!this.isSessionValid(sessionId)) {
172249
+ return null;
172250
+ }
172251
+ return this.sessions.get(sessionId) ?? null;
172252
+ }
172253
+ getActiveSessionCount() {
172254
+ return this.sessions.size;
172255
+ }
172256
+ startCleanupInterval() {
172257
+ if (this.cleanupIntervalId) {
172258
+ return;
172259
+ }
172260
+ this.cleanupIntervalId = setInterval(() => {
172261
+ this.cleanupStaleSessions();
172262
+ }, this.cleanupIntervalMs);
172263
+ logger.info("Session cleanup interval started", {
172264
+ ...requestContextService.createRequestContext({
172265
+ operation: "SessionManager.startCleanupInterval"
172266
+ }),
172267
+ cleanupIntervalMs: this.cleanupIntervalMs,
172268
+ staleTimeoutMs: this.staleTimeoutMs
172269
+ });
172270
+ }
172271
+ stopCleanupInterval() {
172272
+ if (this.cleanupIntervalId) {
172273
+ clearInterval(this.cleanupIntervalId);
172274
+ this.cleanupIntervalId = null;
172275
+ logger.info("Session cleanup interval stopped", {
172276
+ ...requestContextService.createRequestContext({
172277
+ operation: "SessionManager.stopCleanupInterval"
172278
+ })
172279
+ });
172280
+ }
172281
+ }
172282
+ cleanupStaleSessions() {
172283
+ const now2 = Date.now();
172284
+ const sessionsBefore = this.sessions.size;
172285
+ let removedCount = 0;
172286
+ for (const [sessionId, metadata] of this.sessions.entries()) {
172287
+ const age = now2 - metadata.lastActivityAt;
172288
+ if (age > this.staleTimeoutMs) {
172289
+ this.sessions.delete(sessionId);
172290
+ removedCount++;
172291
+ }
172292
+ }
172293
+ if (removedCount > 0) {
172294
+ logger.notice("Cleaned up stale sessions", {
172295
+ ...requestContextService.createRequestContext({
172296
+ operation: "SessionManager.cleanupStaleSessions"
172297
+ }),
172298
+ removedCount,
172299
+ sessionsBefore,
172300
+ sessionsAfter: this.sessions.size
172301
+ });
172302
+ }
172303
+ }
172304
+ clearAllSessions() {
172305
+ const count = this.sessions.size;
172306
+ this.sessions.clear();
172307
+ logger.warning("All sessions cleared", {
172308
+ ...requestContextService.createRequestContext({
172309
+ operation: "SessionManager.clearAllSessions"
172310
+ }),
172311
+ clearedCount: count
172312
+ });
172313
+ }
172314
+ }
172315
+
172052
172316
  // src/mcp-server/transports/http/httpTransport.ts
172053
172317
  init_utils();
172054
172318
 
@@ -172065,6 +172329,11 @@ function createHttpApp(mcpServer, parentContext) {
172065
172329
  ...parentContext,
172066
172330
  component: "HttpTransportSetup"
172067
172331
  };
172332
+ const sessionManager = SessionManager.getInstance(config.mcpStatefulSessionStaleTimeoutMs);
172333
+ logger.info("Session manager initialized", {
172334
+ ...transportContext,
172335
+ staleTimeoutMs: config.mcpStatefulSessionStaleTimeoutMs
172336
+ });
172068
172337
  const allowedOrigin = Array.isArray(config.mcpAllowedOrigins) && config.mcpAllowedOrigins.length > 0 ? config.mcpAllowedOrigins : "*";
172069
172338
  app.use("*", cors({
172070
172339
  origin: allowedOrigin,
@@ -172113,6 +172382,35 @@ function createHttpApp(mcpServer, parentContext) {
172113
172382
  } else {
172114
172383
  logger.info("Authentication is disabled; MCP endpoint is unprotected.", transportContext);
172115
172384
  }
172385
+ app.delete(config.mcpHttpEndpointPath, (c) => {
172386
+ const sessionId = c.req.header("mcp-session-id");
172387
+ if (!sessionId) {
172388
+ return c.json({
172389
+ jsonrpc: "2.0",
172390
+ error: {
172391
+ code: -32600,
172392
+ message: "Mcp-Session-Id header required for DELETE"
172393
+ },
172394
+ id: null
172395
+ }, 400);
172396
+ }
172397
+ const terminated = sessionManager.terminateSession(sessionId);
172398
+ if (!terminated) {
172399
+ return c.json({
172400
+ jsonrpc: "2.0",
172401
+ error: {
172402
+ code: -32001,
172403
+ message: "Session not found or already expired"
172404
+ },
172405
+ id: null
172406
+ }, 404);
172407
+ }
172408
+ logger.info("Session terminated via DELETE", {
172409
+ ...transportContext,
172410
+ sessionId
172411
+ });
172412
+ return c.body(null, 204);
172413
+ });
172116
172414
  app.all(config.mcpHttpEndpointPath, async (c) => {
172117
172415
  const protocolVersion = c.req.header("mcp-protocol-version") ?? "2025-03-26";
172118
172416
  logger.debug("Handling MCP request.", {
@@ -172128,12 +172426,50 @@ function createHttpApp(mcpServer, parentContext) {
172128
172426
  protocolVersion,
172129
172427
  supportedVersions
172130
172428
  });
172429
+ return c.json({
172430
+ jsonrpc: "2.0",
172431
+ error: {
172432
+ code: -32600,
172433
+ message: `Unsupported MCP protocol version: ${protocolVersion}`,
172434
+ data: {
172435
+ requested: protocolVersion,
172436
+ supported: supportedVersions
172437
+ }
172438
+ },
172439
+ id: null
172440
+ }, 400);
172131
172441
  }
172132
172442
  const sessionId = c.req.header("mcp-session-id") ?? randomUUID();
172443
+ if (c.req.header("mcp-session-id") && !sessionManager.isSessionValid(sessionId)) {
172444
+ logger.warning("Invalid or expired session ID", {
172445
+ ...transportContext,
172446
+ sessionId
172447
+ });
172448
+ return c.json({
172449
+ jsonrpc: "2.0",
172450
+ error: {
172451
+ code: -32001,
172452
+ message: "Session expired or invalid. Please reinitialize."
172453
+ },
172454
+ id: null
172455
+ }, 404);
172456
+ }
172457
+ if (!c.req.header("mcp-session-id")) {
172458
+ logger.debug("New session will be created", {
172459
+ ...transportContext,
172460
+ sessionId
172461
+ });
172462
+ } else {
172463
+ sessionManager.touchSession(sessionId);
172464
+ }
172133
172465
  const transport = new McpSessionTransport(sessionId);
172134
172466
  const handleRpc = async () => {
172135
172467
  await mcpServer.connect(transport);
172136
172468
  const response = await transport.handleRequest(c);
172469
+ if (response && !c.req.header("mcp-session-id")) {
172470
+ const store = authContext.getStore();
172471
+ sessionManager.createSession(sessionId, store?.authInfo.clientId, store?.authInfo.tenantId);
172472
+ }
172137
172473
  if (response) {
172138
172474
  return response;
172139
172475
  }
@@ -172229,6 +172565,9 @@ async function stopHttpTransport(server, parentContext) {
172229
172565
  transportType: "Http"
172230
172566
  };
172231
172567
  logger.info("Attempting to stop http transport...", operationContext);
172568
+ const sessionManager = SessionManager.getInstance();
172569
+ sessionManager.stopCleanupInterval();
172570
+ logger.info("Session cleanup interval stopped", operationContext);
172232
172571
  return new Promise((resolve, reject) => {
172233
172572
  server.close((err) => {
172234
172573
  if (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/git-mcp-server",
3
- "version": "2.4.8",
3
+ "version": "2.5.1",
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",
@@ -66,33 +66,12 @@
66
66
  "zod": "3.23.8",
67
67
  "typescript": "5.9.3"
68
68
  },
69
- "dependencies": {
69
+ "devDependencies": {
70
+ "@cloudflare/workers-types": "^4.20251011.0",
71
+ "@eslint/js": "^9.37.0",
70
72
  "@hono/mcp": "^0.1.4",
71
73
  "@hono/node-server": "^1.19.5",
72
74
  "@modelcontextprotocol/sdk": "^1.20.0",
73
- "@supabase/supabase-js": "^2.75.0",
74
- "axios": "^1.12.2",
75
- "chrono-node": "^2.9.0",
76
- "dotenv": "^17.2.3",
77
- "fast-xml-parser": "^5.3.0",
78
- "hono": "^4.9.12",
79
- "ignore": "^7.0.5",
80
- "jose": "^6.1.0",
81
- "js-yaml": "^4.1.0",
82
- "node-cron": "^4.2.1",
83
- "openai": "^6.3.0",
84
- "papaparse": "^5.5.3",
85
- "partial-json": "^0.1.7",
86
- "pdf-lib": "^1.17.1",
87
- "pino": "^10.0.0",
88
- "pino-pretty": "^13.1.2",
89
- "reflect-metadata": "^0.2.2",
90
- "repomix": "^1.7.0",
91
- "sanitize-html": "^2.17.0",
92
- "tslib": "^2.8.1",
93
- "tsyringe": "^4.10.0",
94
- "validator": "13.15.15",
95
- "zod": "^3.23.8",
96
75
  "@opentelemetry/api": "^1.9.0",
97
76
  "@opentelemetry/auto-instrumentations-node": "^0.65.0",
98
77
  "@opentelemetry/exporter-metrics-otlp-http": "^0.206.0",
@@ -102,11 +81,8 @@
102
81
  "@opentelemetry/sdk-metrics": "^2.1.0",
103
82
  "@opentelemetry/sdk-node": "^0.206.0",
104
83
  "@opentelemetry/sdk-trace-node": "^2.1.0",
105
- "@opentelemetry/semantic-conventions": "^1.37.0"
106
- },
107
- "devDependencies": {
108
- "@cloudflare/workers-types": "^4.20251011.0",
109
- "@eslint/js": "^9.37.0",
84
+ "@opentelemetry/semantic-conventions": "^1.37.0",
85
+ "@supabase/supabase-js": "^2.75.0",
110
86
  "@types/bun": "^1.3.0",
111
87
  "@types/js-yaml": "^4.0.9",
112
88
  "@types/node": "^24.7.2",
@@ -117,21 +93,43 @@
117
93
  "@vitest/coverage-v8": "3.2.4",
118
94
  "ajv": "^8.17.1",
119
95
  "ajv-formats": "^3.0.1",
96
+ "axios": "^1.12.2",
120
97
  "bun-types": "^1.3.0",
98
+ "chrono-node": "^2.9.0",
121
99
  "clipboardy": "^5.0.0",
122
100
  "depcheck": "^1.4.7",
101
+ "dotenv": "^17.2.3",
123
102
  "eslint": "^9.37.0",
124
103
  "execa": "^9.6.0",
104
+ "fast-xml-parser": "^5.3.0",
125
105
  "globals": "^16.4.0",
106
+ "hono": "^4.9.12",
126
107
  "husky": "^9.1.7",
108
+ "ignore": "^7.0.5",
109
+ "jose": "^6.1.0",
110
+ "js-yaml": "^4.1.0",
127
111
  "msw": "^2.11.5",
112
+ "node-cron": "^4.2.1",
113
+ "openai": "^6.3.0",
114
+ "papaparse": "^5.5.3",
115
+ "partial-json": "^0.1.7",
116
+ "pdf-lib": "^1.17.1",
117
+ "pino": "^10.0.0",
118
+ "pino-pretty": "^13.1.2",
128
119
  "prettier": "^3.6.2",
120
+ "reflect-metadata": "^0.2.2",
121
+ "repomix": "^1.7.0",
122
+ "sanitize-html": "^2.17.0",
123
+ "tslib": "^2.8.1",
124
+ "tsyringe": "^4.10.0",
129
125
  "typedoc": "^0.28.14",
130
126
  "typescript": "^5.9.3",
131
127
  "typescript-eslint": "8.46.0",
128
+ "validator": "13.15.15",
132
129
  "vite": "7.1.9",
133
130
  "vite-tsconfig-paths": "^5.1.4",
134
- "vitest": "^3.2.4"
131
+ "vitest": "^3.2.4",
132
+ "zod": "^3.23.8"
135
133
  },
136
134
  "keywords": [
137
135
  "ai-agent",