@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.
- package/LICENSE +201 -0
- package/README.md +383 -0
- package/build/index.js +54 -0
- package/build/resources/descriptors.js +77 -0
- package/build/resources/diff.js +241 -0
- package/build/resources/file.js +222 -0
- package/build/resources/history.js +242 -0
- package/build/resources/index.js +99 -0
- package/build/resources/repository.js +286 -0
- package/build/server.js +120 -0
- package/build/services/error-service.js +73 -0
- package/build/services/git-service.js +965 -0
- package/build/tools/advanced.js +526 -0
- package/build/tools/branch.js +296 -0
- package/build/tools/index.js +29 -0
- package/build/tools/remote.js +279 -0
- package/build/tools/repository.js +170 -0
- package/build/tools/workdir.js +445 -0
- package/build/types/git.js +7 -0
- package/build/utils/global-settings.js +64 -0
- package/build/utils/validation.js +108 -0
- package/package.json +39 -0
|
@@ -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,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
|
+
}
|