@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,77 @@
1
+ /**
2
+ * Resource Descriptors
3
+ * ===================
4
+ *
5
+ * This module defines descriptors for Git MCP resources.
6
+ * These descriptions help both users and LLMs understand
7
+ * what each resource does and what data it returns.
8
+ */
9
+ /**
10
+ * Map of resource descriptors keyed by resource ID
11
+ */
12
+ export const resourceDescriptors = {
13
+ // Repository resources
14
+ "repository-info": {
15
+ name: "Repository Information",
16
+ description: "Basic Git repository information including current branch, status, and reference details",
17
+ mimeType: "application/json"
18
+ },
19
+ "repository-branches": {
20
+ name: "Repository Branches",
21
+ description: "List of all branches in the repository with current branch indicator",
22
+ mimeType: "application/json"
23
+ },
24
+ "repository-remotes": {
25
+ name: "Repository Remotes",
26
+ description: "List of all configured remote repositories with their URLs",
27
+ mimeType: "application/json"
28
+ },
29
+ "repository-tags": {
30
+ name: "Repository Tags",
31
+ description: "List of all tags in the repository with their references",
32
+ mimeType: "application/json"
33
+ },
34
+ // File resources
35
+ "file-at-ref": {
36
+ name: "File Content",
37
+ description: "The content of a specific file at a given Git reference",
38
+ mimeType: "text/plain"
39
+ },
40
+ "directory-listing": {
41
+ name: "Directory Listing",
42
+ description: "List of files and directories at a specific path and reference",
43
+ mimeType: "application/json"
44
+ },
45
+ // Diff resources
46
+ "diff-refs": {
47
+ name: "Reference Diff",
48
+ description: "Diff between two Git references (commits, branches, tags)",
49
+ mimeType: "text/plain"
50
+ },
51
+ "diff-unstaged": {
52
+ name: "Unstaged Changes Diff",
53
+ description: "Diff of all unstaged changes in the working directory",
54
+ mimeType: "text/plain"
55
+ },
56
+ "diff-staged": {
57
+ name: "Staged Changes Diff",
58
+ description: "Diff of all staged changes in the index",
59
+ mimeType: "text/plain"
60
+ },
61
+ // History resources
62
+ "commit-log": {
63
+ name: "Commit History",
64
+ description: "Commit history log with author, date, and message details",
65
+ mimeType: "application/json"
66
+ },
67
+ "file-blame": {
68
+ name: "File Blame",
69
+ description: "Line-by-line attribution showing which commit last modified each line",
70
+ mimeType: "text/plain"
71
+ },
72
+ "commit-show": {
73
+ name: "Commit Details",
74
+ description: "Detailed information about a specific commit including diff changes",
75
+ mimeType: "text/plain"
76
+ }
77
+ };
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Diff Resources
3
+ * =============
4
+ *
5
+ * MCP resources for exposing Git diff information.
6
+ */
7
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ /**
11
+ * Helper function to ensure a variable is treated as a string
12
+ *
13
+ * @param value - The value to convert to string
14
+ * @returns A string representation of the value
15
+ */
16
+ function ensureString(value) {
17
+ return Array.isArray(value) ? value[0] : value;
18
+ }
19
+ /**
20
+ * Registers diff resources with the MCP server
21
+ *
22
+ * @param server - MCP server instance
23
+ * @param resourceDescriptors - Resource descriptors for metadata
24
+ */
25
+ export function setupDiffResources(server, resourceDescriptors) {
26
+ // Diff between two refs
27
+ server.resource("diff-refs", new ResourceTemplate("git://repo/{repoPath}/diff/{fromRef}/{toRef}?path={path}", { list: undefined }), {
28
+ name: "Reference Diff",
29
+ description: "Returns a diff between two Git references (commits, branches, tags)",
30
+ mimeType: "application/json" // Corrected MIME type
31
+ },
32
+ // Returns a diff between two references (branches, tags, or commits) with optional path filter
33
+ async (uri, variables) => {
34
+ try {
35
+ // Handle variables which might be arrays
36
+ const repoPathStr = ensureString(variables.repoPath);
37
+ const fromRefStr = ensureString(variables.fromRef);
38
+ const toRefStr = variables.toRef ? ensureString(variables.toRef) : 'HEAD';
39
+ const pathStr = variables.path ? ensureString(variables.path) : undefined;
40
+ // Normalize paths
41
+ const normalizedRepoPath = PathValidation.normalizePath(decodeURIComponent(repoPathStr));
42
+ const gitService = new GitService(normalizedRepoPath);
43
+ // Check if the path is a Git repository
44
+ const isRepo = await gitService.isGitRepository();
45
+ if (!isRepo) {
46
+ return {
47
+ contents: [{
48
+ uri: uri.href,
49
+ text: JSON.stringify({
50
+ error: "Not a Git repository",
51
+ repoPath: normalizedRepoPath,
52
+ fromRef: fromRefStr,
53
+ toRef: toRefStr,
54
+ path: pathStr
55
+ }, null, 2),
56
+ mimeType: "application/json"
57
+ }]
58
+ };
59
+ }
60
+ // Get diff
61
+ const diffResult = await gitService.getDiff(fromRefStr, toRefStr, pathStr);
62
+ if (!diffResult.resultSuccessful) {
63
+ return {
64
+ contents: [{
65
+ uri: uri.href,
66
+ text: JSON.stringify({
67
+ error: diffResult.resultError.errorMessage,
68
+ repoPath: normalizedRepoPath,
69
+ fromRef: fromRefStr,
70
+ toRef: toRefStr,
71
+ path: pathStr
72
+ }, null, 2),
73
+ mimeType: "application/json"
74
+ }]
75
+ };
76
+ }
77
+ return {
78
+ contents: [{
79
+ uri: uri.href,
80
+ text: JSON.stringify({
81
+ repoPath: normalizedRepoPath,
82
+ fromRef: fromRefStr,
83
+ toRef: toRefStr,
84
+ path: pathStr,
85
+ diff: diffResult.resultData
86
+ }, null, 2),
87
+ mimeType: "application/json"
88
+ }]
89
+ };
90
+ }
91
+ catch (error) {
92
+ return {
93
+ contents: [{
94
+ uri: uri.href,
95
+ text: JSON.stringify({
96
+ error: error instanceof Error ? error.message : String(error),
97
+ repoPath: ensureString(variables.repoPath),
98
+ fromRef: ensureString(variables.fromRef),
99
+ toRef: variables.toRef ? ensureString(variables.toRef) : 'HEAD',
100
+ path: variables.path ? ensureString(variables.path) : undefined
101
+ }, null, 2),
102
+ mimeType: "application/json"
103
+ }]
104
+ };
105
+ }
106
+ });
107
+ // Diff in working directory (unstaged changes)
108
+ server.resource("diff-unstaged", new ResourceTemplate("git://repo/{repoPath}/diff-unstaged?path={path}", { list: undefined }), {
109
+ name: "Unstaged Changes Diff",
110
+ description: "Returns a diff of all unstaged changes in the working directory",
111
+ mimeType: "text/plain"
112
+ },
113
+ // Returns a diff of all unstaged changes (between working directory and index) with optional path filter
114
+ async (uri, variables) => {
115
+ try {
116
+ // Handle variables which might be arrays
117
+ const repoPathStr = ensureString(variables.repoPath);
118
+ const pathStr = variables.path ? ensureString(variables.path) : undefined;
119
+ // Normalize paths
120
+ const normalizedRepoPath = PathValidation.normalizePath(decodeURIComponent(repoPathStr));
121
+ const gitService = new GitService(normalizedRepoPath);
122
+ // Check if the path is a Git repository
123
+ const isRepo = await gitService.isGitRepository();
124
+ if (!isRepo) {
125
+ return {
126
+ contents: [{
127
+ uri: uri.href,
128
+ text: JSON.stringify({
129
+ error: "Not a Git repository",
130
+ repoPath: normalizedRepoPath,
131
+ path: pathStr
132
+ }, null, 2),
133
+ mimeType: "application/json"
134
+ }]
135
+ };
136
+ }
137
+ // Get unstaged diff
138
+ const diffResult = await gitService.getUnstagedDiff(pathStr);
139
+ if (!diffResult.resultSuccessful) {
140
+ return {
141
+ contents: [{
142
+ uri: uri.href,
143
+ text: JSON.stringify({
144
+ error: diffResult.resultError.errorMessage,
145
+ repoPath: normalizedRepoPath,
146
+ path: pathStr
147
+ }, null, 2),
148
+ mimeType: "application/json"
149
+ }]
150
+ };
151
+ }
152
+ return {
153
+ contents: [{
154
+ uri: uri.href,
155
+ text: diffResult.resultData,
156
+ mimeType: "text/plain"
157
+ }]
158
+ };
159
+ }
160
+ catch (error) {
161
+ return {
162
+ contents: [{
163
+ uri: uri.href,
164
+ text: JSON.stringify({
165
+ error: error instanceof Error ? error.message : String(error),
166
+ repoPath: ensureString(variables.repoPath),
167
+ path: variables.path ? ensureString(variables.path) : undefined
168
+ }, null, 2),
169
+ mimeType: "application/json"
170
+ }]
171
+ };
172
+ }
173
+ });
174
+ // Diff staged changes
175
+ server.resource("diff-staged", new ResourceTemplate("git://repo/{repoPath}/diff-staged?path={path}", { list: undefined }), {
176
+ name: "Staged Changes Diff",
177
+ description: "Returns a diff of all staged changes in the index",
178
+ mimeType: "text/plain"
179
+ },
180
+ // Returns a diff of all staged changes (between index and HEAD) with optional path filter
181
+ async (uri, variables) => {
182
+ try {
183
+ // Handle variables which might be arrays
184
+ const repoPathStr = ensureString(variables.repoPath);
185
+ const pathStr = variables.path ? ensureString(variables.path) : undefined;
186
+ // Normalize paths
187
+ const normalizedRepoPath = PathValidation.normalizePath(decodeURIComponent(repoPathStr));
188
+ const gitService = new GitService(normalizedRepoPath);
189
+ // Check if the path is a Git repository
190
+ const isRepo = await gitService.isGitRepository();
191
+ if (!isRepo) {
192
+ return {
193
+ contents: [{
194
+ uri: uri.href,
195
+ text: JSON.stringify({
196
+ error: "Not a Git repository",
197
+ repoPath: normalizedRepoPath,
198
+ path: pathStr
199
+ }, null, 2),
200
+ mimeType: "application/json"
201
+ }]
202
+ };
203
+ }
204
+ // Get staged diff
205
+ const diffResult = await gitService.getStagedDiff(pathStr);
206
+ if (!diffResult.resultSuccessful) {
207
+ return {
208
+ contents: [{
209
+ uri: uri.href,
210
+ text: JSON.stringify({
211
+ error: diffResult.resultError.errorMessage,
212
+ repoPath: normalizedRepoPath,
213
+ path: pathStr
214
+ }, null, 2),
215
+ mimeType: "application/json"
216
+ }]
217
+ };
218
+ }
219
+ return {
220
+ contents: [{
221
+ uri: uri.href,
222
+ text: diffResult.resultData,
223
+ mimeType: "text/plain"
224
+ }]
225
+ };
226
+ }
227
+ catch (error) {
228
+ return {
229
+ contents: [{
230
+ uri: uri.href,
231
+ text: JSON.stringify({
232
+ error: error instanceof Error ? error.message : String(error),
233
+ repoPath: ensureString(variables.repoPath),
234
+ path: variables.path ? ensureString(variables.path) : undefined
235
+ }, null, 2),
236
+ mimeType: "application/json"
237
+ }]
238
+ };
239
+ }
240
+ });
241
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * File Resources
3
+ * =============
4
+ *
5
+ * MCP resources for exposing Git file contents at specific references.
6
+ */
7
+ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { GitService } from '../services/git-service.js';
9
+ import { PathValidation } from '../utils/validation.js';
10
+ import path from 'path';
11
+ /**
12
+ * Helper function to ensure a variable is treated as a string
13
+ *
14
+ * @param value - The value to convert to string
15
+ * @returns A string representation of the value
16
+ */
17
+ function ensureString(value) {
18
+ return Array.isArray(value) ? value[0] : value;
19
+ }
20
+ /**
21
+ * Registers file resources with the MCP server
22
+ *
23
+ * @param server - MCP server instance
24
+ * @param resourceDescriptors - Resource descriptors for metadata
25
+ */
26
+ export function setupFileResources(server, resourceDescriptors) {
27
+ // File contents at a specific reference
28
+ server.resource("file-at-ref", new ResourceTemplate("git://repo/{repoPath}/file/{filePath}?ref={ref}", { list: undefined }), {
29
+ name: "File Content",
30
+ description: "Returns the content of a specific file at a given Git reference",
31
+ mimeType: "text/plain"
32
+ },
33
+ // Returns the content of a specific file at a given reference (branch, tag, or commit)
34
+ async (uri, variables) => {
35
+ try {
36
+ // Handle variables which might be arrays
37
+ const repoPathStr = ensureString(variables.repoPath);
38
+ const filePathStr = ensureString(variables.filePath);
39
+ const refStr = variables.ref ? ensureString(variables.ref) : 'HEAD';
40
+ // Normalize paths
41
+ const normalizedRepoPath = PathValidation.normalizePath(decodeURIComponent(repoPathStr));
42
+ const normalizedFilePath = PathValidation.normalizePath(decodeURIComponent(filePathStr));
43
+ const gitService = new GitService(normalizedRepoPath);
44
+ // Check if the path is a Git repository
45
+ const isRepo = await gitService.isGitRepository();
46
+ if (!isRepo) {
47
+ return {
48
+ contents: [{
49
+ uri: uri.href,
50
+ text: JSON.stringify({
51
+ error: "Not a Git repository",
52
+ repoPath: normalizedRepoPath,
53
+ filePath: normalizedFilePath,
54
+ ref: refStr
55
+ }, null, 2),
56
+ mimeType: "application/json"
57
+ }]
58
+ };
59
+ }
60
+ // Get file content at reference
61
+ const fileResult = await gitService.getFileAtRef(normalizedFilePath, refStr);
62
+ if (!fileResult.resultSuccessful) {
63
+ return {
64
+ contents: [{
65
+ uri: uri.href,
66
+ text: JSON.stringify({
67
+ error: fileResult.resultError.errorMessage,
68
+ repoPath: normalizedRepoPath,
69
+ filePath: normalizedFilePath,
70
+ ref: refStr
71
+ }, null, 2),
72
+ mimeType: "application/json"
73
+ }]
74
+ };
75
+ }
76
+ // Detect MIME type based on file extension
77
+ const fileExtension = path.extname(normalizedFilePath).toLowerCase();
78
+ let mimeType = "text/plain";
79
+ // Simple MIME type detection
80
+ if (['.js', '.ts', '.jsx', '.tsx'].includes(fileExtension)) {
81
+ mimeType = "application/javascript";
82
+ }
83
+ else if (['.html', '.htm'].includes(fileExtension)) {
84
+ mimeType = "text/html";
85
+ }
86
+ else if (fileExtension === '.css') {
87
+ mimeType = "text/css";
88
+ }
89
+ else if (fileExtension === '.json') {
90
+ mimeType = "application/json";
91
+ }
92
+ else if (['.md', '.markdown'].includes(fileExtension)) {
93
+ mimeType = "text/markdown";
94
+ }
95
+ else if (['.xml', '.svg'].includes(fileExtension)) {
96
+ mimeType = "application/xml";
97
+ }
98
+ else if (['.yml', '.yaml'].includes(fileExtension)) {
99
+ mimeType = "text/yaml";
100
+ }
101
+ // Return the file content directly
102
+ return {
103
+ contents: [{
104
+ uri: uri.href,
105
+ text: fileResult.resultData,
106
+ mimeType
107
+ }]
108
+ };
109
+ }
110
+ catch (error) {
111
+ return {
112
+ contents: [{
113
+ uri: uri.href,
114
+ text: JSON.stringify({
115
+ error: error instanceof Error ? error.message : String(error),
116
+ repoPath: ensureString(variables.repoPath),
117
+ filePath: ensureString(variables.filePath),
118
+ ref: variables.ref ? ensureString(variables.ref) : 'HEAD'
119
+ }, null, 2),
120
+ mimeType: "application/json"
121
+ }]
122
+ };
123
+ }
124
+ });
125
+ // List files in a directory at a specific reference
126
+ server.resource("directory-listing", new ResourceTemplate("git://repo/{repoPath}/ls/{dirPath}?ref={ref}", { list: undefined }), {
127
+ name: "Directory Listing",
128
+ description: "Returns a list of files and directories at a specific path and reference",
129
+ mimeType: "application/json"
130
+ },
131
+ // Returns a list of files and directories within a specific directory at a given reference
132
+ async (uri, variables) => {
133
+ try {
134
+ // Handle variables which might be arrays
135
+ const repoPathStr = ensureString(variables.repoPath);
136
+ const dirPathStr = ensureString(variables.dirPath || '');
137
+ const refStr = variables.ref ? ensureString(variables.ref) : 'HEAD';
138
+ // Normalize paths
139
+ const normalizedRepoPath = PathValidation.normalizePath(decodeURIComponent(repoPathStr));
140
+ const normalizedDirPath = PathValidation.normalizePath(decodeURIComponent(dirPathStr));
141
+ const gitService = new GitService(normalizedRepoPath);
142
+ // Check if the path is a Git repository
143
+ const isRepo = await gitService.isGitRepository();
144
+ if (!isRepo) {
145
+ return {
146
+ contents: [{
147
+ uri: uri.href,
148
+ text: JSON.stringify({
149
+ error: "Not a Git repository",
150
+ repoPath: normalizedRepoPath,
151
+ dirPath: normalizedDirPath,
152
+ ref: refStr
153
+ }, null, 2),
154
+ mimeType: "application/json"
155
+ }]
156
+ };
157
+ }
158
+ // Use Git command to get directory listing
159
+ try {
160
+ // Use the listFilesAtRef method from GitService
161
+ const filesResult = await gitService.listFilesAtRef(normalizedDirPath, refStr);
162
+ if (!filesResult.resultSuccessful) {
163
+ return {
164
+ contents: [{
165
+ uri: uri.href,
166
+ text: JSON.stringify({
167
+ error: filesResult.resultError.errorMessage,
168
+ repoPath: normalizedRepoPath,
169
+ dirPath: normalizedDirPath,
170
+ ref: refStr
171
+ }, null, 2),
172
+ mimeType: "application/json"
173
+ }]
174
+ };
175
+ }
176
+ // The listFilesAtRef method now returns only immediate children,
177
+ // so no need to strip prefixes here.
178
+ const files = filesResult.resultData;
179
+ return {
180
+ contents: [{
181
+ uri: uri.href,
182
+ text: JSON.stringify({
183
+ repoPath: normalizedRepoPath,
184
+ dirPath: normalizedDirPath,
185
+ ref: refStr,
186
+ files
187
+ }, null, 2),
188
+ mimeType: "application/json"
189
+ }]
190
+ };
191
+ }
192
+ catch (gitError) {
193
+ return {
194
+ contents: [{
195
+ uri: uri.href,
196
+ text: JSON.stringify({
197
+ error: gitError instanceof Error ? gitError.message : String(gitError),
198
+ repoPath: normalizedRepoPath,
199
+ dirPath: normalizedDirPath,
200
+ ref: refStr
201
+ }, null, 2),
202
+ mimeType: "application/json"
203
+ }]
204
+ };
205
+ }
206
+ }
207
+ catch (error) {
208
+ return {
209
+ contents: [{
210
+ uri: uri.href,
211
+ text: JSON.stringify({
212
+ error: error instanceof Error ? error.message : String(error),
213
+ repoPath: ensureString(variables.repoPath),
214
+ dirPath: variables.dirPath ? ensureString(variables.dirPath) : '',
215
+ ref: variables.ref ? ensureString(variables.ref) : 'HEAD'
216
+ }, null, 2),
217
+ mimeType: "application/json"
218
+ }]
219
+ };
220
+ }
221
+ });
222
+ }