@cyanheads/git-mcp-server 2.8.1 → 2.8.3

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 +170 -103
  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.1-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.3-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.8.1",
15338
+ version: "2.8.3",
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), {
@@ -194057,6 +194057,25 @@ async function resolveWorkingDirectory(pathInput, appContext, storage) {
194057
194057
  });
194058
194058
  return sanitizedPath;
194059
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
+ }
194060
194079
 
194061
194080
  // src/mcp-server/tools/utils/toolHandlerFactory.ts
194062
194081
  function validateSdkContext(ctx) {
@@ -196243,7 +196262,8 @@ var InputSchema23 = exports_external.object({
196243
196262
  tags: exports_external.boolean().default(false).describe("Push all tags to the remote."),
196244
196263
  dryRun: DryRunSchema,
196245
196264
  delete: exports_external.boolean().default(false).describe("Delete the specified remote branch."),
196246
- 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.).")
196247
196267
  });
196248
196268
  var OutputSchema24 = exports_external.object({
196249
196269
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
@@ -196254,6 +196274,21 @@ var OutputSchema24 = exports_external.object({
196254
196274
  rejectedRefs: exports_external.array(exports_external.string()).describe("References that were rejected by the remote.")
196255
196275
  });
196256
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
+ }
196257
196292
  const pushOptions = {};
196258
196293
  if (input.remote !== undefined) {
196259
196294
  pushOptions.remote = input.remote;
@@ -196414,7 +196449,8 @@ var InputSchema25 = exports_external.object({
196414
196449
  path: PathSchema,
196415
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)."),
196416
196451
  target: CommitRefSchema.optional().describe("Target commit to reset to (default: HEAD)."),
196417
- 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.).")
196418
196454
  });
196419
196455
  var OutputSchema26 = exports_external.object({
196420
196456
  success: exports_external.boolean().describe("Indicates if the operation was successful."),
@@ -196423,6 +196459,16 @@ var OutputSchema26 = exports_external.object({
196423
196459
  filesReset: exports_external.array(exports_external.string()).describe("Files that were affected by the reset.")
196424
196460
  });
196425
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
+ }
196426
196472
  const resetOptions = {
196427
196473
  mode: input.mode
196428
196474
  };
@@ -201929,6 +201975,7 @@ class SessionManager {
201929
201975
  cleanupIntervalId = null;
201930
201976
  staleTimeoutMs;
201931
201977
  cleanupIntervalMs;
201978
+ onSessionExpired = null;
201932
201979
  constructor(staleTimeoutMs = 30 * 60 * 1000, cleanupIntervalMs = 5 * 60 * 1000) {
201933
201980
  this.staleTimeoutMs = staleTimeoutMs;
201934
201981
  this.cleanupIntervalMs = cleanupIntervalMs;
@@ -201984,6 +202031,7 @@ class SessionManager {
201984
202031
  staleTimeoutMs: this.staleTimeoutMs
201985
202032
  });
201986
202033
  this.sessions.delete(sessionId);
202034
+ this.onSessionExpired?.(sessionId);
201987
202035
  return false;
201988
202036
  }
201989
202037
  return true;
@@ -202051,6 +202099,7 @@ class SessionManager {
202051
202099
  const age = now2 - metadata.lastActivityAt;
202052
202100
  if (age > this.staleTimeoutMs) {
202053
202101
  this.sessions.delete(sessionId);
202102
+ this.onSessionExpired?.(sessionId);
202054
202103
  removedCount++;
202055
202104
  }
202056
202105
  }
@@ -202079,26 +202128,36 @@ class SessionManager {
202079
202128
 
202080
202129
  // src/mcp-server/transports/http/httpTransport.ts
202081
202130
  init_utils();
202082
-
202083
- class McpSessionTransport extends StreamableHTTPTransport {
202084
- sessionId;
202085
- constructor(sessionId) {
202086
- super();
202087
- this.sessionId = sessionId;
202088
- }
202089
- }
202090
- function createHttpApp(mcpServer, parentContext) {
202131
+ function createHttpApp(createMcpServer, parentContext) {
202091
202132
  const app = new Hono2;
202092
202133
  const transportContext = {
202093
202134
  ...parentContext,
202094
202135
  component: "HttpTransportSetup"
202095
202136
  };
202137
+ const transports = new Map;
202096
202138
  const sessionManager = SessionManager.getInstance(config2.mcpStatefulSessionStaleTimeoutMs);
202139
+ sessionManager.onSessionExpired = (sessionId) => {
202140
+ const transport = transports.get(sessionId);
202141
+ if (transport) {
202142
+ transport.close().catch((err) => {
202143
+ logger.warning("Failed to close transport for expired session", {
202144
+ ...transportContext,
202145
+ sessionId,
202146
+ error: err instanceof Error ? err.message : String(err)
202147
+ });
202148
+ });
202149
+ transports.delete(sessionId);
202150
+ }
202151
+ };
202097
202152
  logger.info("Session manager initialized", {
202098
202153
  ...transportContext,
202099
202154
  staleTimeoutMs: config2.mcpStatefulSessionStaleTimeoutMs
202100
202155
  });
202101
- const allowedOrigin = Array.isArray(config2.mcpAllowedOrigins) && config2.mcpAllowedOrigins.length > 0 ? config2.mcpAllowedOrigins : "*";
202156
+ const explicitOrigins = config2.mcpAllowedOrigins;
202157
+ const allowedOrigin = explicitOrigins && explicitOrigins.length > 0 ? explicitOrigins : "*";
202158
+ if (allowedOrigin === "*" && config2.environment === "production") {
202159
+ 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);
202160
+ }
202102
202161
  app.use("*", cors({
202103
202162
  origin: allowedOrigin,
202104
202163
  allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
@@ -202125,19 +202184,6 @@ function createHttpApp(mcpServer, parentContext) {
202125
202184
  ...config2.oauthJwksUri && { jwks_uri: config2.oauthJwksUri }
202126
202185
  });
202127
202186
  });
202128
- app.get(config2.mcpHttpEndpointPath, (c) => {
202129
- return c.json({
202130
- status: "ok",
202131
- server: {
202132
- name: config2.mcpServerName,
202133
- version: config2.mcpServerVersion,
202134
- description: config2.mcpServerDescription,
202135
- environment: config2.environment,
202136
- transport: config2.mcpTransportType,
202137
- sessionMode: config2.mcpSessionMode
202138
- }
202139
- });
202140
- });
202141
202187
  const authStrategy = createAuthStrategy();
202142
202188
  if (authStrategy) {
202143
202189
  const authMiddleware = createAuthMiddleware(authStrategy);
@@ -202146,113 +202192,134 @@ function createHttpApp(mcpServer, parentContext) {
202146
202192
  } else {
202147
202193
  logger.info("Authentication is disabled; MCP endpoint is unprotected.", transportContext);
202148
202194
  }
202149
- app.delete(config2.mcpHttpEndpointPath, (c) => {
202150
- const sessionId = c.req.header("mcp-session-id");
202151
- if (!sessionId) {
202152
- return c.json({
202195
+ const getSessionTransport = (sessionId) => {
202196
+ if (!sessionManager.isSessionValid(sessionId)) {
202197
+ const stale = transports.get(sessionId);
202198
+ if (stale) {
202199
+ stale.close().catch(() => {});
202200
+ transports.delete(sessionId);
202201
+ }
202202
+ return Response.json({
202153
202203
  jsonrpc: "2.0",
202154
202204
  error: {
202155
- code: -32600,
202156
- message: "Mcp-Session-Id header required for DELETE"
202205
+ code: -32001,
202206
+ message: "Session expired or invalid. Please reinitialize."
202157
202207
  },
202158
202208
  id: null
202159
- }, 400);
202209
+ }, { status: 404 });
202160
202210
  }
202161
- const terminated = sessionManager.terminateSession(sessionId);
202162
- if (!terminated) {
202163
- return c.json({
202211
+ const transport = transports.get(sessionId);
202212
+ if (!transport) {
202213
+ return Response.json({
202164
202214
  jsonrpc: "2.0",
202165
202215
  error: {
202166
202216
  code: -32001,
202167
- message: "Session not found or already expired"
202217
+ message: "Session not found. Please reinitialize."
202168
202218
  },
202169
202219
  id: null
202170
- }, 404);
202220
+ }, { status: 404 });
202171
202221
  }
202172
- logger.info("Session terminated via DELETE", {
202173
- ...transportContext,
202174
- sessionId
202175
- });
202176
- return c.body(null, 204);
202177
- });
202178
- app.all(config2.mcpHttpEndpointPath, async (c) => {
202179
- const protocolVersion = c.req.header("mcp-protocol-version") ?? "2025-03-26";
202180
- logger.debug("Handling MCP request.", {
202181
- ...transportContext,
202182
- path: c.req.path,
202183
- method: c.req.method,
202184
- protocolVersion
202185
- });
202186
- const supportedVersions = ["2025-03-26", "2025-06-18"];
202187
- if (!supportedVersions.includes(protocolVersion)) {
202188
- logger.warning("Unsupported MCP protocol version requested.", {
202189
- ...transportContext,
202190
- protocolVersion,
202191
- supportedVersions
202222
+ sessionManager.touchSession(sessionId);
202223
+ return transport;
202224
+ };
202225
+ app.get(config2.mcpHttpEndpointPath, async (c) => {
202226
+ const sessionId = c.req.header("mcp-session-id");
202227
+ if (!sessionId) {
202228
+ return c.json({
202229
+ status: "ok",
202230
+ server: {
202231
+ name: config2.mcpServerName,
202232
+ version: config2.mcpServerVersion,
202233
+ description: config2.mcpServerDescription,
202234
+ transport: config2.mcpTransportType,
202235
+ sessionMode: config2.mcpSessionMode
202236
+ }
202192
202237
  });
202238
+ }
202239
+ const transportOrError = getSessionTransport(sessionId);
202240
+ if (transportOrError instanceof Response)
202241
+ return transportOrError;
202242
+ const response = await transportOrError.handleRequest(c);
202243
+ return response ?? c.body(null, 204);
202244
+ });
202245
+ app.delete(config2.mcpHttpEndpointPath, async (c) => {
202246
+ const sessionId = c.req.header("mcp-session-id");
202247
+ if (!sessionId) {
202193
202248
  return c.json({
202194
202249
  jsonrpc: "2.0",
202195
202250
  error: {
202196
202251
  code: -32600,
202197
- message: `Unsupported MCP protocol version: ${protocolVersion}`,
202198
- data: {
202199
- requested: protocolVersion,
202200
- supported: supportedVersions
202201
- }
202252
+ message: "Mcp-Session-Id header required for DELETE"
202202
202253
  },
202203
202254
  id: null
202204
202255
  }, 400);
202205
202256
  }
202206
- const sessionId = c.req.header("mcp-session-id") ?? randomUUID();
202207
- if (c.req.header("mcp-session-id") && !sessionManager.isSessionValid(sessionId)) {
202208
- logger.warning("Invalid or expired session ID", {
202209
- ...transportContext,
202210
- sessionId
202211
- });
202257
+ const transport = transports.get(sessionId);
202258
+ if (!transport) {
202212
202259
  return c.json({
202213
202260
  jsonrpc: "2.0",
202214
202261
  error: {
202215
202262
  code: -32001,
202216
- message: "Session expired or invalid. Please reinitialize."
202263
+ message: "Session not found or already expired"
202217
202264
  },
202218
202265
  id: null
202219
202266
  }, 404);
202220
202267
  }
202221
- if (!c.req.header("mcp-session-id")) {
202222
- logger.debug("New session will be created", {
202223
- ...transportContext,
202224
- sessionId
202268
+ const response = await transport.handleRequest(c);
202269
+ transports.delete(sessionId);
202270
+ sessionManager.terminateSession(sessionId);
202271
+ logger.info("Session terminated via DELETE", {
202272
+ ...transportContext,
202273
+ sessionId
202274
+ });
202275
+ return response ?? c.body(null, 204);
202276
+ });
202277
+ app.post(config2.mcpHttpEndpointPath, async (c) => {
202278
+ logger.debug("Handling MCP POST request.", {
202279
+ ...transportContext,
202280
+ path: c.req.path
202281
+ });
202282
+ const sessionId = c.req.header("mcp-session-id");
202283
+ const handleRequest = async () => {
202284
+ if (sessionId) {
202285
+ const transportOrError = getSessionTransport(sessionId);
202286
+ if (transportOrError instanceof Response)
202287
+ return transportOrError;
202288
+ const response2 = await transportOrError.handleRequest(c);
202289
+ return response2 ?? c.body(null, 204);
202290
+ }
202291
+ const server = await createMcpServer();
202292
+ const transport = new StreamableHTTPTransport({
202293
+ sessionIdGenerator: () => randomUUID(),
202294
+ onsessioninitialized: (sid) => {
202295
+ transports.set(sid, transport);
202296
+ const store = authContext.getStore();
202297
+ sessionManager.createSession(sid, store?.authInfo.clientId, store?.authInfo.tenantId);
202298
+ logger.debug("New MCP session initialized", {
202299
+ ...transportContext,
202300
+ sessionId: sid
202301
+ });
202302
+ },
202303
+ onsessionclosed: (sid) => {
202304
+ transports.delete(sid);
202305
+ sessionManager.terminateSession(sid);
202306
+ logger.debug("MCP session closed via transport", {
202307
+ ...transportContext,
202308
+ sessionId: sid
202309
+ });
202310
+ }
202225
202311
  });
202226
- } else {
202227
- sessionManager.touchSession(sessionId);
202228
- }
202229
- const transport = new McpSessionTransport(sessionId);
202230
- const handleRpc = async () => {
202231
- await mcpServer.connect(transport);
202312
+ await server.connect(transport);
202232
202313
  const response = await transport.handleRequest(c);
202233
- if (response && !c.req.header("mcp-session-id")) {
202234
- const store = authContext.getStore();
202235
- sessionManager.createSession(sessionId, store?.authInfo.clientId, store?.authInfo.tenantId);
202236
- }
202237
- if (response) {
202238
- return response;
202239
- }
202240
- return c.body(null, 204);
202314
+ return response ?? c.body(null, 204);
202241
202315
  };
202242
202316
  try {
202243
202317
  const store = authContext.getStore();
202244
202318
  if (store) {
202245
- return await authContext.run(store, handleRpc);
202319
+ return await authContext.run(store, handleRequest);
202246
202320
  }
202247
- return await handleRpc();
202321
+ return await handleRequest();
202248
202322
  } catch (err) {
202249
- await transport.close?.().catch((closeErr) => {
202250
- logger.warning("Failed to close transport after error", {
202251
- ...transportContext,
202252
- sessionId,
202253
- error: closeErr instanceof Error ? closeErr.message : String(closeErr)
202254
- });
202255
- });
202256
202323
  throw err instanceof Error ? err : new Error(String(err));
202257
202324
  }
202258
202325
  });
@@ -202311,13 +202378,13 @@ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentCont
202311
202378
  tryBind(initialPort, 1);
202312
202379
  });
202313
202380
  }
202314
- async function startHttpTransport(mcpServer, parentContext) {
202381
+ async function startHttpTransport(createMcpServer, parentContext) {
202315
202382
  const transportContext = {
202316
202383
  ...parentContext,
202317
202384
  component: "HttpTransportStart"
202318
202385
  };
202319
202386
  logger.info("Starting HTTP transport.", transportContext);
202320
- const app = createHttpApp(mcpServer, transportContext);
202387
+ const app = createHttpApp(createMcpServer, transportContext);
202321
202388
  const server = await startHttpServerWithRetry(app, config2.mcpHttpPort, config2.mcpHttpHost, config2.mcpHttpMaxPortRetries, transportContext);
202322
202389
  logger.info("HTTP transport started successfully.", transportContext);
202323
202390
  return server;
@@ -202494,10 +202561,10 @@ class TransportManager {
202494
202561
  transport: this.config.mcpTransportType
202495
202562
  });
202496
202563
  this.logger.info(`Starting transport: ${this.config.mcpTransportType}`, context);
202497
- const mcpServer = await this.createMcpServer();
202498
202564
  if (this.config.mcpTransportType === "http") {
202499
- this.serverInstance = await startHttpTransport(mcpServer, context);
202565
+ this.serverInstance = await startHttpTransport(this.createMcpServer, context);
202500
202566
  } else if (this.config.mcpTransportType === "stdio") {
202567
+ const mcpServer = await this.createMcpServer();
202501
202568
  this.serverInstance = await startStdioTransport(mcpServer, context);
202502
202569
  } else {
202503
202570
  const transportType = String(this.config.mcpTransportType);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/git-mcp-server",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
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",