@cyanheads/git-mcp-server 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +10 -8
  2. package/dist/config/index.js +10 -2
  3. package/dist/mcp-server/server.js +33 -32
  4. package/dist/mcp-server/tools/gitAdd/logic.js +18 -45
  5. package/dist/mcp-server/tools/gitBranch/logic.js +39 -34
  6. package/dist/mcp-server/tools/gitCheckout/logic.js +17 -12
  7. package/dist/mcp-server/tools/gitCherryPick/logic.js +22 -15
  8. package/dist/mcp-server/tools/gitClean/logic.js +11 -8
  9. package/dist/mcp-server/tools/gitClone/logic.js +15 -11
  10. package/dist/mcp-server/tools/gitCommit/logic.js +29 -65
  11. package/dist/mcp-server/tools/gitDiff/logic.js +29 -14
  12. package/dist/mcp-server/tools/gitFetch/logic.js +13 -12
  13. package/dist/mcp-server/tools/gitInit/logic.js +12 -9
  14. package/dist/mcp-server/tools/gitLog/logic.js +17 -30
  15. package/dist/mcp-server/tools/gitMerge/logic.js +17 -12
  16. package/dist/mcp-server/tools/gitPull/logic.js +13 -14
  17. package/dist/mcp-server/tools/gitPush/logic.js +19 -21
  18. package/dist/mcp-server/tools/gitRebase/logic.js +29 -20
  19. package/dist/mcp-server/tools/gitRemote/logic.js +15 -15
  20. package/dist/mcp-server/tools/gitReset/logic.js +11 -10
  21. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +6 -4
  22. package/dist/mcp-server/tools/gitShow/logic.js +9 -8
  23. package/dist/mcp-server/tools/gitStash/logic.js +16 -17
  24. package/dist/mcp-server/tools/gitStatus/logic.js +10 -8
  25. package/dist/mcp-server/tools/gitTag/logic.js +15 -15
  26. package/dist/mcp-server/tools/gitWorktree/logic.js +54 -38
  27. package/dist/mcp-server/transports/auth/core/authContext.js +24 -0
  28. package/dist/mcp-server/transports/auth/core/authTypes.js +5 -0
  29. package/dist/mcp-server/transports/auth/core/authUtils.js +45 -0
  30. package/dist/mcp-server/transports/auth/index.js +9 -0
  31. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +149 -0
  32. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +127 -0
  33. package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
  34. package/dist/mcp-server/transports/httpTransport.js +149 -495
  35. package/dist/mcp-server/transports/stdioTransport.js +18 -48
  36. package/package.json +4 -13
  37. package/dist/mcp-server/transports/authentication/authMiddleware.js +0 -167
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the input schema for the git_push tool using Zod
12
12
  export const GitPushInputSchema = z.object({
13
13
  path: z
@@ -111,45 +111,43 @@ export async function pushGitChanges(input, context) {
111
111
  }
112
112
  try {
113
113
  // Construct the git push command
114
- let command = `git -C "${targetPath}" push`;
114
+ const args = ["-C", targetPath, "push"];
115
115
  if (input.force) {
116
- command += " --force";
116
+ args.push("--force");
117
117
  }
118
118
  else if (input.forceWithLease) {
119
- command += " --force-with-lease";
119
+ args.push("--force-with-lease");
120
120
  }
121
121
  if (input.setUpstream) {
122
- command += " --set-upstream";
122
+ args.push("--set-upstream");
123
123
  }
124
124
  if (input.tags) {
125
- command += " --tags";
125
+ args.push("--tags");
126
126
  }
127
127
  if (input.delete) {
128
- command += " --delete";
128
+ args.push("--delete");
129
129
  }
130
130
  // Add remote and branch specification
131
- const remote = input.remote
132
- ? input.remote.replace(/[^a-zA-Z0-9_.\-/]/g, "")
133
- : "origin"; // Default to origin
134
- command += ` ${remote}`;
131
+ const remote = input.remote || "origin"; // Default to origin
132
+ args.push(remote);
135
133
  if (input.branch) {
136
- const localBranch = input.branch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
137
- command += ` ${localBranch}`;
138
134
  if (input.remoteBranch && !input.delete) {
139
- // remoteBranch only makes sense if not deleting
140
- const remoteBranch = input.remoteBranch.replace(/[^a-zA-Z0-9_.\-/]/g, "");
141
- command += `:${remoteBranch}`;
135
+ args.push(`${input.branch}:${input.remoteBranch}`);
136
+ }
137
+ else {
138
+ args.push(input.branch);
142
139
  }
143
140
  }
144
141
  else if (!input.tags && !input.delete) {
145
142
  // If no branch, tags, or delete specified, push the current branch by default
146
- // Git might handle this automatically, but being explicit can be clearer
147
- // command += ' HEAD'; // Or let git figure out the default push behavior
148
143
  logger.debug("No specific branch, tags, or delete specified. Relying on default git push behavior for current branch.", { ...context, operation });
149
144
  }
150
- logger.debug(`Executing command: ${command}`, { ...context, operation });
145
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
146
+ ...context,
147
+ operation,
148
+ });
151
149
  // Execute command. Note: Git push often uses stderr for progress and success messages.
152
- const { stdout, stderr } = await execAsync(command);
150
+ const { stdout, stderr } = await execFileAsync("git", args);
153
151
  logger.debug(`Git push stdout: ${stdout}`, { ...context, operation });
154
152
  if (stderr) {
155
153
  logger.debug(`Git push stderr: ${stderr}`, { ...context, operation });
@@ -1,4 +1,4 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  // Import utils from barrel (logger from ../utils/internal/logger.js)
@@ -7,7 +7,7 @@ import { logger } from "../../../utils/index.js";
7
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Keep direct import for types-global
8
8
  // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
9
  import { sanitization } from "../../../utils/index.js";
10
- const execAsync = promisify(exec);
10
+ const execFileAsync = promisify(execFile);
11
11
  // Define the BASE input schema for the git_rebase tool using Zod
12
12
  export const GitRebaseBaseSchema = z.object({
13
13
  path: z
@@ -117,39 +117,48 @@ export async function gitRebaseLogic(input, context) {
117
117
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
118
118
  }
119
119
  try {
120
- let command = `git -C "${targetPath}" rebase`;
120
+ const args = ["-C", targetPath, "rebase"];
121
121
  switch (input.mode) {
122
122
  case "start":
123
- if (input.interactive)
124
- command += " -i";
125
- if (input.strategy)
126
- command += ` --strategy=${input.strategy}`;
127
- if (input.strategyOption)
128
- command += ` -X${input.strategyOption}`; // Note: -X for strategy options
129
- if (input.onto)
130
- command += ` --onto "${input.onto.replace(/"/g, '\\"')}"`;
123
+ if (input.interactive) {
124
+ args.push("-i");
125
+ }
126
+ if (input.strategy) {
127
+ args.push(`--strategy=${input.strategy}`);
128
+ }
129
+ if (input.strategyOption) {
130
+ args.push(`-X${input.strategyOption}`);
131
+ } // Note: -X for strategy options
132
+ if (input.onto) {
133
+ args.push("--onto", input.onto);
134
+ }
131
135
  // Upstream is required by refine unless interactive
132
- if (input.upstream)
133
- command += ` "${input.upstream.replace(/"/g, '\\"')}"`;
134
- if (input.branch)
135
- command += ` "${input.branch.replace(/"/g, '\\"')}"`;
136
+ if (input.upstream) {
137
+ args.push(input.upstream);
138
+ }
139
+ if (input.branch) {
140
+ args.push(input.branch);
141
+ }
136
142
  break;
137
143
  case "continue":
138
- command += " --continue";
144
+ args.push("--continue");
139
145
  break;
140
146
  case "abort":
141
- command += " --abort";
147
+ args.push("--abort");
142
148
  break;
143
149
  case "skip":
144
- command += " --skip";
150
+ args.push("--skip");
145
151
  break;
146
152
  default:
147
153
  // Should not happen due to Zod validation
148
154
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid mode: ${input.mode}`, { context, operation });
149
155
  }
150
- logger.debug(`Executing command: ${command}`, { ...context, operation });
156
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
157
+ ...context,
158
+ operation,
159
+ });
151
160
  try {
152
- const { stdout, stderr } = await execAsync(command);
161
+ const { stdout, stderr } = await execFileAsync("git", args);
153
162
  const output = stdout + stderr;
154
163
  const message = `Rebase ${input.mode} executed successfully. Output: ${output.trim()}`;
155
164
  logger.info(message, { ...context, operation, path: targetPath });
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // Logger (./utils/internal/logger.js) & RequestContext (./utils/internal/requestContext.js) & sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the input schema for the git_remote tool using Zod
8
8
  export const GitRemoteInputSchema = z.object({
9
9
  path: z
@@ -79,16 +79,16 @@ export async function gitRemoteLogic(input, context) {
79
79
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
80
80
  }
81
81
  try {
82
- let command;
82
+ let args;
83
83
  let result;
84
84
  switch (input.mode) {
85
85
  case "list":
86
- command = `git -C "${targetPath}" remote -v`;
87
- logger.debug(`Executing command: ${command}`, {
86
+ args = ["-C", targetPath, "remote", "-v"];
87
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
88
88
  ...context,
89
89
  operation,
90
90
  });
91
- const { stdout: listStdout } = await execAsync(command);
91
+ const { stdout: listStdout } = await execFileAsync("git", args);
92
92
  const remotes = [];
93
93
  const lines = listStdout.trim().split("\n");
94
94
  const remoteMap = new Map();
@@ -127,12 +127,12 @@ export async function gitRemoteLogic(input, context) {
127
127
  if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
128
128
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
129
129
  }
130
- command = `git -C "${targetPath}" remote add "${input.name}" "${input.url}"`;
131
- logger.debug(`Executing command: ${command}`, {
130
+ args = ["-C", targetPath, "remote", "add", input.name, input.url];
131
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
132
132
  ...context,
133
133
  operation,
134
134
  });
135
- await execAsync(command);
135
+ await execFileAsync("git", args);
136
136
  result = {
137
137
  success: true,
138
138
  mode: "add",
@@ -146,12 +146,12 @@ export async function gitRemoteLogic(input, context) {
146
146
  if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
147
147
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
148
148
  }
149
- command = `git -C "${targetPath}" remote remove "${input.name}"`;
150
- logger.debug(`Executing command: ${command}`, {
149
+ args = ["-C", targetPath, "remote", "remove", input.name];
150
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
151
151
  ...context,
152
152
  operation,
153
153
  });
154
- await execAsync(command);
154
+ await execFileAsync("git", args);
155
155
  result = {
156
156
  success: true,
157
157
  mode: "remove",
@@ -165,12 +165,12 @@ export async function gitRemoteLogic(input, context) {
165
165
  if (!/^[a-zA-Z0-9_.-]+$/.test(input.name)) {
166
166
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid remote name: ${input.name}`, { context, operation });
167
167
  }
168
- command = `git -C "${targetPath}" remote show "${input.name}"`;
169
- logger.debug(`Executing command: ${command}`, {
168
+ args = ["-C", targetPath, "remote", "show", input.name];
169
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
170
170
  ...context,
171
171
  operation,
172
172
  });
173
- const { stdout: showStdout } = await execAsync(command);
173
+ const { stdout: showStdout } = await execFileAsync("git", args);
174
174
  result = { success: true, mode: "show", details: showStdout.trim() };
175
175
  break;
176
176
  default:
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the reset modes
8
8
  const ResetModeEnum = z.enum(["soft", "mixed", "hard", "merge", "keep"]);
9
9
  // Define the input schema for the git_reset tool using Zod
@@ -73,24 +73,25 @@ export async function resetGitState(input, context) {
73
73
  throw error;
74
74
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error });
75
75
  }
76
- // Basic sanitization for commit ref
77
- const safeCommit = input.commit?.replace(/[`$&;*()|<>]/g, "");
78
76
  try {
79
77
  // Construct the git reset command
80
- let command = `git -C "${targetPath}" reset`;
78
+ const args = ["-C", targetPath, "reset"];
81
79
  if (input.mode) {
82
- command += ` --${input.mode}`;
80
+ args.push(`--${input.mode}`);
83
81
  }
84
- if (safeCommit) {
85
- command += ` ${safeCommit}`;
82
+ if (input.commit) {
83
+ args.push(input.commit);
86
84
  }
87
85
  // Handling file paths requires careful command construction, often without a commit ref.
88
86
  // Example: `git reset HEAD -- path/to/file` or `git reset -- path/to/file` (unstages)
89
87
  // For simplicity, this initial version focuses on resetting the whole HEAD/index/tree.
90
88
  // Add file path logic here if needed, adjusting command structure.
91
- logger.debug(`Executing command: ${command}`, { ...context, operation });
89
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
90
+ ...context,
91
+ operation,
92
+ });
92
93
  // Execute command. Reset output is often minimal on success, but stderr might indicate issues.
93
- const { stdout, stderr } = await execAsync(command);
94
+ const { stdout, stderr } = await execFileAsync("git", args);
94
95
  logger.debug(`Git reset stdout: ${stdout}`, { ...context, operation });
95
96
  if (stderr) {
96
97
  // Log stderr as info, as it often contains the primary status message
@@ -1,10 +1,10 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import fs from "fs/promises";
3
3
  import { promisify } from "util";
4
4
  import { z } from "zod";
5
5
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
6
6
  import { logger, sanitization } from "../../../utils/index.js"; // RequestContext (./utils/internal/requestContext.js), logger (./utils/internal/logger.js), sanitization (./utils/security/sanitization.js)
7
- const execAsync = promisify(exec);
7
+ const execFileAsync = promisify(execFile);
8
8
  // Define the Zod schema for input validation
9
9
  export const GitSetWorkingDirInputSchema = z.object({
10
10
  path: z
@@ -70,7 +70,7 @@ export async function gitSetWorkingDirLogic(input, context) {
70
70
  let isGitRepo = false;
71
71
  let initializedRepo = false;
72
72
  try {
73
- const { stdout } = await execAsync("git rev-parse --is-inside-work-tree", {
73
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
74
74
  cwd: sanitizedPath,
75
75
  });
76
76
  if (stdout.trim() === "true") {
@@ -94,7 +94,9 @@ export async function gitSetWorkingDirLogic(input, context) {
94
94
  if (!isGitRepo && input.initializeIfNotPresent) {
95
95
  logger.info(`Path is not a Git repository. Attempting to initialize (initializeIfNotPresent=true) with initial branch 'main'.`, { ...context, operation, path: sanitizedPath });
96
96
  try {
97
- await execAsync("git init --initial-branch=main", { cwd: sanitizedPath });
97
+ await execFileAsync("git", ["init", "--initial-branch=main"], {
98
+ cwd: sanitizedPath,
99
+ });
98
100
  initializedRepo = true;
99
101
  isGitRepo = true; // Now it is a git repo
100
102
  logger.info('Successfully initialized Git repository with initial branch "main".', { ...context, operation, path: sanitizedPath });
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the input schema for the git_show tool using Zod
8
8
  // No refinements needed here, so we don't need a separate BaseSchema
9
9
  export const GitShowInputSchema = z.object({
@@ -88,15 +88,16 @@ export async function gitShowLogic(input, context) {
88
88
  }
89
89
  try {
90
90
  // Construct the refspec, combining ref and filePath if needed
91
- const refSpec = input.filePath
92
- ? `${input.ref}:"${input.filePath}"`
93
- : `"${input.ref}"`;
91
+ const refSpec = input.filePath ? `${input.ref}:${input.filePath}` : input.ref;
94
92
  // Construct the command
95
- const command = `git -C "${targetPath}" show ${refSpec}`;
96
- logger.debug(`Executing command: ${command}`, { ...context, operation });
93
+ const args = ["-C", targetPath, "show", refSpec];
94
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
95
+ ...context,
96
+ operation,
97
+ });
97
98
  // Execute command. Note: git show might write to stderr for non-error info (like commit details before diff)
98
99
  // We primarily care about stdout for the content. Errors usually have non-zero exit code.
99
- const { stdout, stderr } = await execAsync(command);
100
+ const { stdout, stderr } = await execFileAsync("git", args);
100
101
  if (stderr) {
101
102
  // Log stderr as debug info, as it might contain commit details etc.
102
103
  logger.debug(`Git show command produced stderr (may be informational)`, {
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the BASE input schema for the git_stash tool using Zod
8
8
  export const GitStashBaseSchema = z.object({
9
9
  path: z
@@ -90,16 +90,16 @@ export async function gitStashLogic(input, context) {
90
90
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid stash reference format: ${input.stashRef}. Expected format: stash@{n}`, { context, operation });
91
91
  }
92
92
  try {
93
- let command;
93
+ let args;
94
94
  let result;
95
95
  switch (input.mode) {
96
96
  case "list":
97
- command = `git -C "${targetPath}" stash list`;
98
- logger.debug(`Executing command: ${command}`, {
97
+ args = ["-C", targetPath, "stash", "list"];
98
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
99
99
  ...context,
100
100
  operation,
101
101
  });
102
- const { stdout: listStdout } = await execAsync(command);
102
+ const { stdout: listStdout } = await execFileAsync("git", args);
103
103
  const stashes = listStdout
104
104
  .trim()
105
105
  .split("\n")
@@ -121,13 +121,13 @@ export async function gitStashLogic(input, context) {
121
121
  case "pop":
122
122
  // stashRef is validated by Zod refine
123
123
  const stashRefApplyPop = input.stashRef;
124
- command = `git -C "${targetPath}" stash ${input.mode} ${stashRefApplyPop}`;
125
- logger.debug(`Executing command: ${command}`, {
124
+ args = ["-C", targetPath, "stash", input.mode, stashRefApplyPop];
125
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
126
126
  ...context,
127
127
  operation,
128
128
  });
129
129
  try {
130
- const { stdout, stderr } = await execAsync(command);
130
+ const { stdout, stderr } = await execFileAsync("git", args);
131
131
  // Check stdout/stderr for conflict messages, although exit code 0 usually means success
132
132
  const conflicts = /conflict/i.test(stdout) || /conflict/i.test(stderr);
133
133
  const message = conflicts
@@ -166,12 +166,12 @@ export async function gitStashLogic(input, context) {
166
166
  case "drop":
167
167
  // stashRef is validated by Zod refine
168
168
  const stashRefDrop = input.stashRef;
169
- command = `git -C "${targetPath}" stash drop ${stashRefDrop}`;
170
- logger.debug(`Executing command: ${command}`, {
169
+ args = ["-C", targetPath, "stash", "drop", stashRefDrop];
170
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
171
171
  ...context,
172
172
  operation,
173
173
  });
174
- await execAsync(command);
174
+ await execFileAsync("git", args);
175
175
  result = {
176
176
  success: true,
177
177
  mode: "drop",
@@ -180,16 +180,15 @@ export async function gitStashLogic(input, context) {
180
180
  };
181
181
  break;
182
182
  case "save":
183
- command = `git -C "${targetPath}" stash save`;
183
+ args = ["-C", targetPath, "stash", "save"];
184
184
  if (input.message) {
185
- // Ensure message is properly quoted for the shell
186
- command += ` "${input.message.replace(/"/g, '\\"')}"`;
185
+ args.push(input.message);
187
186
  }
188
- logger.debug(`Executing command: ${command}`, {
187
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
189
188
  ...context,
190
189
  operation,
191
190
  });
192
- const { stdout: saveStdout } = await execAsync(command);
191
+ const { stdout: saveStdout } = await execFileAsync("git", args);
193
192
  const stashCreated = !/no local changes to save/i.test(saveStdout);
194
193
  const saveMessage = stashCreated
195
194
  ? `Changes stashed successfully.` +
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the input schema for the git_status tool using Zod
8
8
  export const GitStatusInputSchema = z.object({
9
9
  path: z
@@ -197,10 +197,12 @@ export async function getGitStatus(input, context) {
197
197
  }
198
198
  try {
199
199
  // Using --porcelain=v1 for stable, scriptable output and -b for branch info
200
- // Ensure the path passed to -C is correctly quoted for the shell
201
- const command = `git -C "${targetPath}" status --porcelain=v1 -b`;
202
- logger.debug(`Executing command: ${command}`, { ...context, operation });
203
- const { stdout, stderr } = await execAsync(command);
200
+ const args = ["-C", targetPath, "status", "--porcelain=v1", "-b"];
201
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
202
+ ...context,
203
+ operation,
204
+ });
205
+ const { stdout, stderr } = await execFileAsync("git", args);
204
206
  if (stderr) {
205
207
  // Log stderr as warning but proceed to parse stdout
206
208
  logger.warning(`Git status command produced stderr (may be informational)`, { ...context, operation, stderr });
@@ -216,8 +218,8 @@ export async function getGitStatus(input, context) {
216
218
  // This handles the case of an empty repo after init but before first commit
217
219
  if (structuredResult.is_clean && !structuredResult.current_branch) {
218
220
  try {
219
- const branchCommand = `git -C "${targetPath}" rev-parse --abbrev-ref HEAD`;
220
- const { stdout: branchStdout } = await execAsync(branchCommand);
221
+ const branchArgs = ["-C", targetPath, "rev-parse", "--abbrev-ref", "HEAD"];
222
+ const { stdout: branchStdout } = await execFileAsync("git", branchArgs);
221
223
  const currentBranchName = branchStdout.trim(); // Renamed variable for clarity
222
224
  if (currentBranchName && currentBranchName !== "HEAD") {
223
225
  structuredResult.current_branch = currentBranchName;
@@ -1,9 +1,9 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { z } from "zod";
4
4
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Direct import for types-global
5
5
  import { logger, sanitization } from "../../../utils/index.js"; // logger (./utils/internal/logger.js), RequestContext (./utils/internal/requestContext.js), sanitization (./utils/security/sanitization.js)
6
- const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
7
  // Define the base input schema for the git_tag tool using Zod
8
8
  // We export this separately to access its .shape for registration
9
9
  export const GitTagBaseSchema = z.object({
@@ -111,16 +111,16 @@ export async function gitTagLogic(input, context) {
111
111
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid commit reference format: ${input.commitRef}`, { context, operation });
112
112
  }
113
113
  try {
114
- let command;
114
+ let args;
115
115
  let result;
116
116
  switch (input.mode) {
117
117
  case "list":
118
- command = `git -C "${targetPath}" tag --list`;
119
- logger.debug(`Executing command: ${command}`, {
118
+ args = ["-C", targetPath, "tag", "--list"];
119
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
120
120
  ...context,
121
121
  operation,
122
122
  });
123
- const { stdout: listStdout } = await execAsync(command);
123
+ const { stdout: listStdout } = await execFileAsync("git", args);
124
124
  const tags = listStdout
125
125
  .trim()
126
126
  .split("\n")
@@ -130,20 +130,20 @@ export async function gitTagLogic(input, context) {
130
130
  case "create":
131
131
  // TagName is validated by Zod refine
132
132
  const tagNameCreate = input.tagName;
133
- command = `git -C "${targetPath}" tag`;
133
+ args = ["-C", targetPath, "tag"];
134
134
  if (input.annotate) {
135
135
  // Message is validated by Zod refine
136
- command += ` -a -m "${input.message.replace(/"/g, '\\"')}"`;
136
+ args.push("-a", "-m", input.message);
137
137
  }
138
- command += ` "${tagNameCreate}"`;
138
+ args.push(tagNameCreate);
139
139
  if (input.commitRef) {
140
- command += ` "${input.commitRef}"`;
140
+ args.push(input.commitRef);
141
141
  }
142
- logger.debug(`Executing command: ${command}`, {
142
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
143
143
  ...context,
144
144
  operation,
145
145
  });
146
- await execAsync(command);
146
+ await execFileAsync("git", args);
147
147
  result = {
148
148
  success: true,
149
149
  mode: "create",
@@ -154,12 +154,12 @@ export async function gitTagLogic(input, context) {
154
154
  case "delete":
155
155
  // TagName is validated by Zod refine
156
156
  const tagNameDelete = input.tagName;
157
- command = `git -C "${targetPath}" tag -d "${tagNameDelete}"`;
158
- logger.debug(`Executing command: ${command}`, {
157
+ args = ["-C", targetPath, "tag", "-d", tagNameDelete];
158
+ logger.debug(`Executing command: git ${args.join(" ")}`, {
159
159
  ...context,
160
160
  operation,
161
161
  });
162
- await execAsync(command);
162
+ await execFileAsync("git", args);
163
163
  result = {
164
164
  success: true,
165
165
  mode: "delete",