@cyanheads/git-mcp-server 1.2.4

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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Repository Tools
3
+ * ===============
4
+ *
5
+ * MCP tools for Git repository operations.
6
+ */
7
+ import { z } from 'zod';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ /**
11
+ * Registers repository tools with the MCP server
12
+ *
13
+ * @param server - MCP server instance
14
+ */
15
+ export function setupRepositoryTools(server) {
16
+ // Initialize a new Git repository
17
+ server.tool("git_init", "Initialize a new Git repository. Creates the necessary directory structure and Git metadata for a new Git repository at the specified path. The repository can be created as a standard repository with a working directory or as a bare repository (typically used for centralized repositories). Creates a 'main' branch by default.", {
18
+ path: z.string().min(1, "Repository path is required").describe("Path to initialize the Git repository in"),
19
+ bare: z.boolean().optional().default(false).describe("Whether to create a bare repository without a working directory"),
20
+ initialBranch: z.string().optional().default("main").describe("Name of the initial branch (defaults to 'main')")
21
+ }, async ({ path, bare, initialBranch }) => {
22
+ try {
23
+ const normalizedPath = PathValidation.normalizePath(path);
24
+ const gitService = new GitService(normalizedPath);
25
+ const result = await gitService.initRepo(bare, initialBranch);
26
+ if (!result.resultSuccessful) {
27
+ return {
28
+ content: [{
29
+ type: "text",
30
+ text: `Error: ${result.resultError.errorMessage}`
31
+ }],
32
+ isError: true
33
+ };
34
+ }
35
+ return {
36
+ content: [{
37
+ type: "text",
38
+ text: `Successfully initialized ${bare ? 'bare ' : ''}Git repository with initial branch '${initialBranch}' at: ${normalizedPath}`
39
+ }]
40
+ };
41
+ }
42
+ catch (error) {
43
+ return {
44
+ content: [{
45
+ type: "text",
46
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
47
+ }],
48
+ isError: true
49
+ };
50
+ }
51
+ });
52
+ // Clone a Git repository
53
+ server.tool("git_clone", "Clone a Git repository. Downloads a repository from a remote location and creates a local copy with all its history. Supports specifying branches, creating shallow clones, and more.", {
54
+ url: z.string().url("Invalid repository URL").describe("URL of the Git repository to clone"),
55
+ path: z.string().min(1, "Destination path is required").describe("Local path where the repository will be cloned"),
56
+ branch: z.string().optional().describe("Specific branch to checkout after cloning"),
57
+ depth: z.number().positive().optional().describe("Create a shallow clone with specified number of commits")
58
+ }, async ({ url, path, branch, depth }) => {
59
+ try {
60
+ const normalizedPath = PathValidation.normalizePath(path);
61
+ const gitService = new GitService(normalizedPath);
62
+ const options = {};
63
+ if (branch)
64
+ options.branch = branch;
65
+ if (depth)
66
+ options.depth = depth;
67
+ const result = await gitService.cloneRepo(url, options);
68
+ if (!result.resultSuccessful) {
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: `Error: ${result.resultError.errorMessage}`
73
+ }],
74
+ isError: true
75
+ };
76
+ }
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: `Successfully cloned repository from ${url} to ${normalizedPath}`
81
+ }]
82
+ };
83
+ }
84
+ catch (error) {
85
+ return {
86
+ content: [{
87
+ type: "text",
88
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
89
+ }],
90
+ isError: true
91
+ };
92
+ }
93
+ });
94
+ // Get repository status
95
+ server.tool("git_status", "Get repository status. Shows the working tree status including tracked/untracked files, modifications, staged changes, and current branch information.", {
96
+ path: z.string().min(1, "Repository path is required").describe("Path to the Git repository")
97
+ }, async ({ path }) => {
98
+ try {
99
+ const normalizedPath = PathValidation.normalizePath(path);
100
+ const gitService = new GitService(normalizedPath);
101
+ // Check if this is a git repository
102
+ const isRepo = await gitService.isGitRepository();
103
+ if (!isRepo) {
104
+ return {
105
+ content: [{
106
+ type: "text",
107
+ text: `Error: Not a Git repository: ${normalizedPath}`
108
+ }],
109
+ isError: true
110
+ };
111
+ }
112
+ const result = await gitService.getStatus();
113
+ if (!result.resultSuccessful) {
114
+ return {
115
+ content: [{
116
+ type: "text",
117
+ text: `Error: ${result.resultError.errorMessage}`
118
+ }],
119
+ isError: true
120
+ };
121
+ }
122
+ const status = result.resultData;
123
+ const isClean = status.isClean();
124
+ let statusOutput = `Status for repository at: ${normalizedPath}\n`;
125
+ statusOutput += `Current branch: ${status.current}\n`;
126
+ if (status.tracking) {
127
+ statusOutput += `Tracking: ${status.tracking}\n`;
128
+ }
129
+ if (isClean) {
130
+ statusOutput += `\nWorking directory clean`;
131
+ }
132
+ else {
133
+ // Show untracked files
134
+ if (status.not_added && status.not_added.length > 0) {
135
+ statusOutput += `\nUntracked files:\n ${status.not_added.join('\n ')}\n`;
136
+ }
137
+ if (status.created.length > 0) {
138
+ statusOutput += `\nNew files:\n ${status.created.join('\n ')}\n`;
139
+ }
140
+ if (status.modified.length > 0) {
141
+ statusOutput += `\nModified files:\n ${status.modified.join('\n ')}\n`;
142
+ }
143
+ if (status.deleted.length > 0) {
144
+ statusOutput += `\nDeleted files:\n ${status.deleted.join('\n ')}\n`;
145
+ }
146
+ if (status.renamed.length > 0) {
147
+ statusOutput += `\nRenamed files:\n ${status.renamed.join('\n ')}\n`;
148
+ }
149
+ if (status.conflicted.length > 0) {
150
+ statusOutput += `\nConflicted files:\n ${status.conflicted.join('\n ')}\n`;
151
+ }
152
+ }
153
+ return {
154
+ content: [{
155
+ type: "text",
156
+ text: statusOutput
157
+ }]
158
+ };
159
+ }
160
+ catch (error) {
161
+ return {
162
+ content: [{
163
+ type: "text",
164
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
165
+ }],
166
+ isError: true
167
+ };
168
+ }
169
+ });
170
+ }
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Working Directory Tools
3
+ * =====================
4
+ *
5
+ * MCP tools for Git working directory operations.
6
+ */
7
+ import { z } from 'zod';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ import { getGlobalSettings } from '../utils/global-settings.js';
11
+ /**
12
+ * Registers working directory tools with the MCP server
13
+ *
14
+ * @param server - MCP server instance
15
+ */
16
+ export function setupWorkdirTools(server) {
17
+ // Set global working directory
18
+ server.tool("git_set_working_dir", "Set a global working directory path for all Git operations. Future tool calls can use '.' as the filepath and it will resolve to this global path. IMPORTANT: Always use a full, absolute path to ensure proper functionality.", {
19
+ path: z.string().min(1, "Working directory path is required").describe("Full, absolute path to use as the global working directory"),
20
+ validateGitRepo: z.boolean().optional().default(true).describe("Whether to validate that the path is a Git repository")
21
+ }, async ({ path, validateGitRepo }) => {
22
+ try {
23
+ const normalizedPath = PathValidation.normalizePath(path);
24
+ // Check if this is a git repository if validation is requested
25
+ if (validateGitRepo) {
26
+ const gitService = new GitService(normalizedPath);
27
+ const isRepo = await gitService.isGitRepository();
28
+ if (!isRepo) {
29
+ return {
30
+ content: [{
31
+ type: "text",
32
+ text: `Error: Not a Git repository: ${normalizedPath}`
33
+ }],
34
+ isError: true
35
+ };
36
+ }
37
+ }
38
+ // Set the global working directory
39
+ getGlobalSettings().setGlobalWorkingDir(normalizedPath);
40
+ return {
41
+ content: [{
42
+ type: "text",
43
+ text: `Successfully set global working directory to: ${normalizedPath}`
44
+ }]
45
+ };
46
+ }
47
+ catch (error) {
48
+ return {
49
+ content: [{
50
+ type: "text",
51
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
52
+ }],
53
+ isError: true
54
+ };
55
+ }
56
+ });
57
+ // Clear global working directory
58
+ server.tool("git_clear_working_dir", "Clear the global working directory setting. Tools will use their explicitly provided path parameters.", {}, async () => {
59
+ try {
60
+ const currentPath = getGlobalSettings().globalWorkingDir;
61
+ getGlobalSettings().setGlobalWorkingDir(null);
62
+ return {
63
+ content: [{
64
+ type: "text",
65
+ text: currentPath
66
+ ? `Successfully cleared global working directory (was: ${currentPath})`
67
+ : "No global working directory was set"
68
+ }]
69
+ };
70
+ }
71
+ catch (error) {
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
76
+ }],
77
+ isError: true
78
+ };
79
+ }
80
+ });
81
+ // Stage files
82
+ server.tool("git_add", "Stage files for commit. Adds file contents to the index (staging area) in preparation for the next commit. Can stage specific files or all changes in the working directory. IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
83
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
84
+ files: z.union([
85
+ z.string().min(1, "File path is required").describe("Path to a file to stage"),
86
+ z.array(z.string().min(1, "File path is required")).describe("Array of file paths to stage")
87
+ ]).optional().default('.').describe("Files to stage for commit, defaults to all changes")
88
+ }, async ({ path, files }) => {
89
+ try {
90
+ const normalizedPath = PathValidation.normalizePath(path);
91
+ const gitService = new GitService(normalizedPath);
92
+ // Check if this is a git repository
93
+ const isRepo = await gitService.isGitRepository();
94
+ if (!isRepo) {
95
+ return {
96
+ content: [{
97
+ type: "text",
98
+ text: `Error: Not a Git repository: ${normalizedPath}`
99
+ }],
100
+ isError: true
101
+ };
102
+ }
103
+ const result = await gitService.stageFiles(files);
104
+ if (!result.resultSuccessful) {
105
+ return {
106
+ content: [{
107
+ type: "text",
108
+ text: `Error: ${result.resultError.errorMessage}`
109
+ }],
110
+ isError: true
111
+ };
112
+ }
113
+ return {
114
+ content: [{
115
+ type: "text",
116
+ text: `Successfully staged ${typeof files === 'string' && files === '.' ? 'all files' :
117
+ (Array.isArray(files) ? `${files.length} files` : `'${files}'`)}`
118
+ }]
119
+ };
120
+ }
121
+ catch (error) {
122
+ return {
123
+ content: [{
124
+ type: "text",
125
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
126
+ }],
127
+ isError: true
128
+ };
129
+ }
130
+ });
131
+ // Unstage files
132
+ server.tool("git_reset", "Unstage files from the index. Removes file contents from the staging area while preserving the working directory changes. The opposite of git_add. IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
133
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
134
+ files: z.union([
135
+ z.string().min(1, "File path is required").describe("Path to a file to unstage"),
136
+ z.array(z.string().min(1, "File path is required")).describe("Array of file paths to unstage")
137
+ ]).optional().default('.').describe("Files to unstage, defaults to all staged changes")
138
+ }, async ({ path, files }) => {
139
+ try {
140
+ const normalizedPath = PathValidation.normalizePath(path);
141
+ const gitService = new GitService(normalizedPath);
142
+ // Check if this is a git repository
143
+ const isRepo = await gitService.isGitRepository();
144
+ if (!isRepo) {
145
+ return {
146
+ content: [{
147
+ type: "text",
148
+ text: `Error: Not a Git repository: ${normalizedPath}`
149
+ }],
150
+ isError: true
151
+ };
152
+ }
153
+ const result = await gitService.unstageFiles(files);
154
+ if (!result.resultSuccessful) {
155
+ return {
156
+ content: [{
157
+ type: "text",
158
+ text: `Error: ${result.resultError.errorMessage}`
159
+ }],
160
+ isError: true
161
+ };
162
+ }
163
+ return {
164
+ content: [{
165
+ type: "text",
166
+ text: `Successfully unstaged ${typeof files === 'string' && files === '.' ? 'all files' :
167
+ (Array.isArray(files) ? `${files.length} files` : `'${files}'`)}`
168
+ }]
169
+ };
170
+ }
171
+ catch (error) {
172
+ return {
173
+ content: [{
174
+ type: "text",
175
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
176
+ }],
177
+ isError: true
178
+ };
179
+ }
180
+ });
181
+ // Commit changes
182
+ server.tool("git_commit", "Commit staged changes to the repository. Creates a new commit containing the current contents of the index with the provided commit message. Supports optional author information, amending previous commits, and creating empty commits. IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
183
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
184
+ message: z.string().min(1, "Commit message is required").describe("Message for the commit"),
185
+ author: z.object({
186
+ name: z.string().optional().describe("Author name for the commit"),
187
+ email: z.string().email("Invalid email").optional().describe("Author email for the commit")
188
+ }).optional().describe("Author information for the commit"),
189
+ allowEmpty: z.boolean().optional().default(false).describe("Allow creating empty commits"),
190
+ amend: z.boolean().optional().default(false).describe("Amend the previous commit instead of creating a new one")
191
+ }, async ({ path, message, author, allowEmpty, amend }) => {
192
+ try {
193
+ const normalizedPath = PathValidation.normalizePath(path);
194
+ const gitService = new GitService(normalizedPath);
195
+ // Check if this is a git repository
196
+ const isRepo = await gitService.isGitRepository();
197
+ if (!isRepo) {
198
+ return {
199
+ content: [{
200
+ type: "text",
201
+ text: `Error: Not a Git repository: ${normalizedPath}`
202
+ }],
203
+ isError: true
204
+ };
205
+ }
206
+ // The GitService constructor and simple-git should automatically use the
207
+ // GIT_AUTHOR_NAME/EMAIL environment variables set in server.ts.
208
+ // No need to fetch global config here again.
209
+ // Pass the provided author object directly, or undefined if not provided.
210
+ const result = await gitService.commit({
211
+ message,
212
+ author: author, // Pass the input author directly
213
+ allowEmpty,
214
+ amend
215
+ });
216
+ if (!result.resultSuccessful) {
217
+ return {
218
+ content: [{
219
+ type: "text",
220
+ text: `Error: ${result.resultError.errorMessage}`
221
+ }],
222
+ isError: true
223
+ };
224
+ }
225
+ return {
226
+ content: [{
227
+ type: "text",
228
+ text: `Successfully committed changes${amend ? ' (amended)' : ''} with message: "${message}"\nCommit hash: ${result.resultData}`
229
+ }]
230
+ };
231
+ }
232
+ catch (error) {
233
+ return {
234
+ content: [{
235
+ type: "text",
236
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
237
+ }],
238
+ isError: true
239
+ };
240
+ }
241
+ });
242
+ // View working directory diff
243
+ server.tool("git_diff_unstaged", "Show unstaged changes in the working directory. Displays the differences between the working directory and the index (staging area). Can be limited to a specific file or show all changed files. IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
244
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
245
+ file: z.string().optional().describe("Specific file to get diff for, or all files if omitted"),
246
+ showUntracked: z.boolean().optional().default(true).describe("Whether to include information about untracked files")
247
+ }, async ({ path, file, showUntracked }) => {
248
+ try {
249
+ const normalizedPath = PathValidation.normalizePath(path);
250
+ const gitService = new GitService(normalizedPath);
251
+ // Check if this is a git repository
252
+ const isRepo = await gitService.isGitRepository();
253
+ if (!isRepo) {
254
+ return {
255
+ content: [{
256
+ type: "text",
257
+ text: `Error: Not a Git repository: ${normalizedPath}`
258
+ }],
259
+ isError: true
260
+ };
261
+ }
262
+ const result = await gitService.getUnstagedDiff(file, showUntracked);
263
+ if (!result.resultSuccessful) {
264
+ return {
265
+ content: [{
266
+ type: "text",
267
+ text: `Error: ${result.resultError.errorMessage}`
268
+ }],
269
+ isError: true
270
+ };
271
+ }
272
+ if (result.resultData.trim() === '') {
273
+ return {
274
+ content: [{
275
+ type: "text",
276
+ text: `No unstaged changes or untracked files${file ? ` in '${file}'` : ''}`
277
+ }]
278
+ };
279
+ }
280
+ return {
281
+ content: [{
282
+ type: "text",
283
+ text: result.resultData
284
+ }]
285
+ };
286
+ }
287
+ catch (error) {
288
+ return {
289
+ content: [{
290
+ type: "text",
291
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
292
+ }],
293
+ isError: true
294
+ };
295
+ }
296
+ });
297
+ // View staged diff
298
+ server.tool("git_diff_staged", "Show staged changes ready for commit. Displays the differences between the index (staging area) and the latest commit. Can be limited to a specific file or show all staged files. IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
299
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
300
+ file: z.string().optional().describe("Specific file to get diff for, or all files if omitted")
301
+ }, async ({ path, file }) => {
302
+ try {
303
+ const normalizedPath = PathValidation.normalizePath(path);
304
+ const gitService = new GitService(normalizedPath);
305
+ // Check if this is a git repository
306
+ const isRepo = await gitService.isGitRepository();
307
+ if (!isRepo) {
308
+ return {
309
+ content: [{
310
+ type: "text",
311
+ text: `Error: Not a Git repository: ${normalizedPath}`
312
+ }],
313
+ isError: true
314
+ };
315
+ }
316
+ const result = await gitService.getStagedDiff(file);
317
+ if (!result.resultSuccessful) {
318
+ return {
319
+ content: [{
320
+ type: "text",
321
+ text: `Error: ${result.resultError.errorMessage}`
322
+ }],
323
+ isError: true
324
+ };
325
+ }
326
+ if (result.resultData.trim() === '') {
327
+ return {
328
+ content: [{
329
+ type: "text",
330
+ text: `No staged changes${file ? ` in '${file}'` : ''}`
331
+ }]
332
+ };
333
+ }
334
+ return {
335
+ content: [{
336
+ type: "text",
337
+ text: result.resultData
338
+ }]
339
+ };
340
+ }
341
+ catch (error) {
342
+ return {
343
+ content: [{
344
+ type: "text",
345
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
346
+ }],
347
+ isError: true
348
+ };
349
+ }
350
+ });
351
+ // Reset to a specific commit
352
+ server.tool("git_reset_commit", "Reset the current branch to a specific commit. This changes where the branch HEAD points to, with different modes affecting the working directory and index differently (hard: discard all changes, soft: keep staged changes, mixed: unstage but keep changes). IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
353
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
354
+ ref: z.string().default("HEAD").describe("Reference to reset to, defaults to HEAD (e.g., commit hash, branch name, or HEAD~1)"),
355
+ mode: z.enum(["hard", "soft", "mixed"]).default("mixed").describe("Reset mode: hard (discard changes), soft (keep staged), or mixed (unstage but keep changes)")
356
+ }, async ({ path, ref, mode }) => {
357
+ try {
358
+ const normalizedPath = PathValidation.normalizePath(path);
359
+ const gitService = new GitService(normalizedPath);
360
+ // Check if this is a git repository
361
+ const isRepo = await gitService.isGitRepository();
362
+ if (!isRepo) {
363
+ return {
364
+ content: [{
365
+ type: "text",
366
+ text: `Error: Not a Git repository: ${normalizedPath}`
367
+ }],
368
+ isError: true
369
+ };
370
+ }
371
+ const result = await gitService.reset(ref, mode);
372
+ if (!result.resultSuccessful) {
373
+ return {
374
+ content: [{
375
+ type: "text",
376
+ text: `Error: ${result.resultError.errorMessage}`
377
+ }],
378
+ isError: true
379
+ };
380
+ }
381
+ return {
382
+ content: [{
383
+ type: "text",
384
+ text: `Successfully reset to ${ref} using mode: ${mode}`
385
+ }]
386
+ };
387
+ }
388
+ catch (error) {
389
+ return {
390
+ content: [{
391
+ type: "text",
392
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
393
+ }],
394
+ isError: true
395
+ };
396
+ }
397
+ });
398
+ // Clean working directory
399
+ server.tool("git_clean", "Remove untracked files from the working directory. Deletes files that aren't tracked by Git, optionally including directories. Use with caution as this operation cannot be undone. IMPORTANT: Always use a full, absolute path to the repository to ensure proper functionality.", {
400
+ path: z.string().min(1, "Repository path is required").describe("Full, absolute path to the Git repository"),
401
+ directories: z.boolean().optional().default(false).describe("Whether to remove untracked directories in addition to files"),
402
+ force: z.boolean().optional().default(false).describe("Force cleaning of files, including ignored files")
403
+ }, async ({ path, directories, force }) => {
404
+ try {
405
+ const normalizedPath = PathValidation.normalizePath(path);
406
+ const gitService = new GitService(normalizedPath);
407
+ // Check if this is a git repository
408
+ const isRepo = await gitService.isGitRepository();
409
+ if (!isRepo) {
410
+ return {
411
+ content: [{
412
+ type: "text",
413
+ text: `Error: Not a Git repository: ${normalizedPath}`
414
+ }],
415
+ isError: true
416
+ };
417
+ }
418
+ const result = await gitService.clean(directories, force);
419
+ if (!result.resultSuccessful) {
420
+ return {
421
+ content: [{
422
+ type: "text",
423
+ text: `Error: ${result.resultError.errorMessage}`
424
+ }],
425
+ isError: true
426
+ };
427
+ }
428
+ return {
429
+ content: [{
430
+ type: "text",
431
+ text: `Successfully cleaned working directory${directories ? ' (including directories)' : ''}`
432
+ }]
433
+ };
434
+ }
435
+ catch (error) {
436
+ return {
437
+ content: [{
438
+ type: "text",
439
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
440
+ }],
441
+ isError: true
442
+ };
443
+ }
444
+ });
445
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Git-Related Type Definitions
3
+ * ============================
4
+ *
5
+ * Type definitions for Git operations and entities used throughout the server.
6
+ */
7
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Global Settings Utility
3
+ * ======================
4
+ *
5
+ * Provides global settings for the Git MCP server, including security configurations.
6
+ * These settings can be used across different tools and services.
7
+ */
8
+ import path from 'path';
9
+ /**
10
+ * Global settings singleton for storing app-wide configuration
11
+ */
12
+ export class GlobalSettings {
13
+ static instance;
14
+ _globalWorkingDir = null;
15
+ _allowedBaseDir;
16
+ /**
17
+ * Private constructor to enforce singleton pattern and validate required settings
18
+ */
19
+ constructor() {
20
+ // Validate and set the allowed base directory from environment variable
21
+ const baseDir = process.env.GIT_MCP_BASE_DIR;
22
+ if (!baseDir) {
23
+ throw new Error('FATAL: GIT_MCP_BASE_DIR environment variable is not set. Server cannot operate securely without a defined base directory.');
24
+ }
25
+ // Normalize the base directory path
26
+ this._allowedBaseDir = path.resolve(baseDir);
27
+ console.log(`[GlobalSettings] Allowed base directory set to: ${this._allowedBaseDir}`);
28
+ }
29
+ /**
30
+ * Get the singleton instance
31
+ */
32
+ static getInstance() {
33
+ if (!GlobalSettings.instance) {
34
+ GlobalSettings.instance = new GlobalSettings();
35
+ }
36
+ return GlobalSettings.instance;
37
+ }
38
+ /**
39
+ * Get the global working directory if set
40
+ */
41
+ get globalWorkingDir() {
42
+ return this._globalWorkingDir;
43
+ }
44
+ /**
45
+ * Get the allowed base directory for sandboxing repository access
46
+ */
47
+ get allowedBaseDir() {
48
+ return this._allowedBaseDir;
49
+ }
50
+ /**
51
+ * Set the global working directory
52
+ *
53
+ * @param path - Path to use as global working directory
54
+ */
55
+ setGlobalWorkingDir(path) {
56
+ this._globalWorkingDir = path;
57
+ }
58
+ }
59
+ /**
60
+ * Helper function to get global settings instance
61
+ */
62
+ export function getGlobalSettings() {
63
+ return GlobalSettings.getInstance();
64
+ }