@cyanheads/git-mcp-server 1.2.4 → 2.0.2
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/README.md +172 -285
- package/dist/config/index.js +69 -0
- package/dist/index.js +135 -0
- package/dist/mcp-server/server.js +572 -0
- package/dist/mcp-server/tools/gitAdd/index.js +7 -0
- package/dist/mcp-server/tools/gitAdd/logic.js +118 -0
- package/dist/mcp-server/tools/gitAdd/registration.js +73 -0
- package/dist/mcp-server/tools/gitBranch/index.js +7 -0
- package/dist/mcp-server/tools/gitBranch/logic.js +180 -0
- package/dist/mcp-server/tools/gitBranch/registration.js +72 -0
- package/dist/mcp-server/tools/gitCheckout/index.js +6 -0
- package/dist/mcp-server/tools/gitCheckout/logic.js +165 -0
- package/dist/mcp-server/tools/gitCheckout/registration.js +78 -0
- package/dist/mcp-server/tools/gitCherryPick/index.js +7 -0
- package/dist/mcp-server/tools/gitCherryPick/logic.js +115 -0
- package/dist/mcp-server/tools/gitCherryPick/registration.js +69 -0
- package/dist/mcp-server/tools/gitClean/index.js +7 -0
- package/dist/mcp-server/tools/gitClean/logic.js +110 -0
- package/dist/mcp-server/tools/gitClean/registration.js +98 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +7 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +35 -0
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +73 -0
- package/dist/mcp-server/tools/gitClone/index.js +7 -0
- package/dist/mcp-server/tools/gitClone/logic.js +136 -0
- package/dist/mcp-server/tools/gitClone/registration.js +44 -0
- package/dist/mcp-server/tools/gitCommit/index.js +7 -0
- package/dist/mcp-server/tools/gitCommit/logic.js +129 -0
- package/dist/mcp-server/tools/gitCommit/registration.js +100 -0
- package/dist/mcp-server/tools/gitDiff/index.js +6 -0
- package/dist/mcp-server/tools/gitDiff/logic.js +114 -0
- package/dist/mcp-server/tools/gitDiff/registration.js +74 -0
- package/dist/mcp-server/tools/gitFetch/index.js +6 -0
- package/dist/mcp-server/tools/gitFetch/logic.js +116 -0
- package/dist/mcp-server/tools/gitFetch/registration.js +71 -0
- package/dist/mcp-server/tools/gitInit/index.js +7 -0
- package/dist/mcp-server/tools/gitInit/logic.js +117 -0
- package/dist/mcp-server/tools/gitInit/registration.js +44 -0
- package/dist/mcp-server/tools/gitLog/index.js +6 -0
- package/dist/mcp-server/tools/gitLog/logic.js +148 -0
- package/dist/mcp-server/tools/gitLog/registration.js +71 -0
- package/dist/mcp-server/tools/gitMerge/index.js +7 -0
- package/dist/mcp-server/tools/gitMerge/logic.js +160 -0
- package/dist/mcp-server/tools/gitMerge/registration.js +77 -0
- package/dist/mcp-server/tools/gitPull/index.js +6 -0
- package/dist/mcp-server/tools/gitPull/logic.js +144 -0
- package/dist/mcp-server/tools/gitPull/registration.js +81 -0
- package/dist/mcp-server/tools/gitPush/index.js +6 -0
- package/dist/mcp-server/tools/gitPush/logic.js +188 -0
- package/dist/mcp-server/tools/gitPush/registration.js +81 -0
- package/dist/mcp-server/tools/gitRebase/index.js +7 -0
- package/dist/mcp-server/tools/gitRebase/logic.js +171 -0
- package/dist/mcp-server/tools/gitRebase/registration.js +72 -0
- package/dist/mcp-server/tools/gitRemote/index.js +7 -0
- package/dist/mcp-server/tools/gitRemote/logic.js +158 -0
- package/dist/mcp-server/tools/gitRemote/registration.js +76 -0
- package/dist/mcp-server/tools/gitReset/index.js +6 -0
- package/dist/mcp-server/tools/gitReset/logic.js +116 -0
- package/dist/mcp-server/tools/gitReset/registration.js +71 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +7 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +91 -0
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +78 -0
- package/dist/mcp-server/tools/gitShow/index.js +7 -0
- package/dist/mcp-server/tools/gitShow/logic.js +99 -0
- package/dist/mcp-server/tools/gitShow/registration.js +83 -0
- package/dist/mcp-server/tools/gitStash/index.js +7 -0
- package/dist/mcp-server/tools/gitStash/logic.js +161 -0
- package/dist/mcp-server/tools/gitStash/registration.js +84 -0
- package/dist/mcp-server/tools/gitStatus/index.js +7 -0
- package/dist/mcp-server/tools/gitStatus/logic.js +215 -0
- package/dist/mcp-server/tools/gitStatus/registration.js +77 -0
- package/dist/mcp-server/tools/gitTag/index.js +7 -0
- package/dist/mcp-server/tools/gitTag/logic.js +142 -0
- package/dist/mcp-server/tools/gitTag/registration.js +84 -0
- package/dist/types-global/errors.js +68 -0
- package/dist/types-global/mcp.js +59 -0
- package/dist/types-global/tool.js +1 -0
- package/dist/utils/errorHandler.js +237 -0
- package/dist/utils/idGenerator.js +148 -0
- package/dist/utils/index.js +11 -0
- package/dist/utils/jsonParser.js +78 -0
- package/dist/utils/logger.js +266 -0
- package/dist/utils/rateLimiter.js +177 -0
- package/dist/utils/requestContext.js +49 -0
- package/dist/utils/sanitization.js +371 -0
- package/dist/utils/tokenCounter.js +124 -0
- package/package.json +62 -17
- package/build/index.js +0 -54
- package/build/resources/descriptors.js +0 -77
- package/build/resources/diff.js +0 -241
- package/build/resources/file.js +0 -222
- package/build/resources/history.js +0 -242
- package/build/resources/index.js +0 -99
- package/build/resources/repository.js +0 -286
- package/build/server.js +0 -120
- package/build/services/error-service.js +0 -73
- package/build/services/git-service.js +0 -965
- package/build/tools/advanced.js +0 -526
- package/build/tools/branch.js +0 -296
- package/build/tools/index.js +0 -29
- package/build/tools/remote.js +0 -279
- package/build/tools/repository.js +0 -170
- package/build/tools/workdir.js +0 -445
- package/build/types/git.js +0 -7
- package/build/utils/global-settings.js +0 -64
- package/build/utils/validation.js +0 -108
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import sanitizeHtml from 'sanitize-html';
|
|
3
|
+
import validator from 'validator';
|
|
4
|
+
import { BaseErrorCode, McpError } from '../types-global/errors.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Sanitization class for handling various input sanitization tasks
|
|
8
|
+
*/
|
|
9
|
+
export class Sanitization {
|
|
10
|
+
static instance;
|
|
11
|
+
/** Default list of sensitive fields for sanitizing logs */
|
|
12
|
+
sensitiveFields = [
|
|
13
|
+
'password', 'token', 'secret', 'key', 'apiKey', 'auth',
|
|
14
|
+
'credential', 'jwt', 'ssn', 'credit', 'card', 'cvv', 'authorization'
|
|
15
|
+
];
|
|
16
|
+
/** Default sanitize-html configuration */
|
|
17
|
+
defaultHtmlSanitizeConfig = {
|
|
18
|
+
allowedTags: [
|
|
19
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
|
|
20
|
+
'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br',
|
|
21
|
+
'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'pre'
|
|
22
|
+
],
|
|
23
|
+
allowedAttributes: {
|
|
24
|
+
'a': ['href', 'name', 'target'],
|
|
25
|
+
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
26
|
+
'*': ['class', 'id', 'style']
|
|
27
|
+
},
|
|
28
|
+
preserveComments: false
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Private constructor to enforce singleton pattern
|
|
32
|
+
*/
|
|
33
|
+
constructor() {
|
|
34
|
+
logger.debug('Sanitization service initialized with modern libraries');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the singleton Sanitization instance
|
|
38
|
+
* @returns Sanitization instance
|
|
39
|
+
*/
|
|
40
|
+
static getInstance() {
|
|
41
|
+
if (!Sanitization.instance) {
|
|
42
|
+
Sanitization.instance = new Sanitization();
|
|
43
|
+
}
|
|
44
|
+
return Sanitization.instance;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Set sensitive fields for log sanitization
|
|
48
|
+
* @param fields Array of field names to consider sensitive
|
|
49
|
+
*/
|
|
50
|
+
setSensitiveFields(fields) {
|
|
51
|
+
this.sensitiveFields = [...new Set([...this.sensitiveFields, ...fields])]; // Ensure uniqueness
|
|
52
|
+
logger.debug('Updated sensitive fields list', { count: this.sensitiveFields.length });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the current list of sensitive fields
|
|
56
|
+
* @returns Array of sensitive field names
|
|
57
|
+
*/
|
|
58
|
+
getSensitiveFields() {
|
|
59
|
+
return [...this.sensitiveFields];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Sanitize HTML content using sanitize-html library
|
|
63
|
+
* @param input HTML string to sanitize
|
|
64
|
+
* @param config Optional custom sanitization config
|
|
65
|
+
* @returns Sanitized HTML
|
|
66
|
+
*/
|
|
67
|
+
sanitizeHtml(input, config) {
|
|
68
|
+
if (!input)
|
|
69
|
+
return '';
|
|
70
|
+
// Create sanitize-html options from our config
|
|
71
|
+
const options = {
|
|
72
|
+
allowedTags: config?.allowedTags || this.defaultHtmlSanitizeConfig.allowedTags,
|
|
73
|
+
allowedAttributes: config?.allowedAttributes || this.defaultHtmlSanitizeConfig.allowedAttributes,
|
|
74
|
+
transformTags: config?.transformTags
|
|
75
|
+
};
|
|
76
|
+
// Handle comments - if preserveComments is true, add '!--' to allowedTags
|
|
77
|
+
if (config?.preserveComments || this.defaultHtmlSanitizeConfig.preserveComments) {
|
|
78
|
+
options.allowedTags = [...(options.allowedTags || []), '!--'];
|
|
79
|
+
}
|
|
80
|
+
return sanitizeHtml(input, options);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Sanitize string input based on context.
|
|
84
|
+
*
|
|
85
|
+
* **Important:** Using `context: 'javascript'` is explicitly disallowed and will throw an `McpError`.
|
|
86
|
+
* This is a security measure to prevent accidental execution or ineffective sanitization of JavaScript code.
|
|
87
|
+
*
|
|
88
|
+
* @param input String to sanitize
|
|
89
|
+
* @param options Sanitization options
|
|
90
|
+
* @returns Sanitized string
|
|
91
|
+
* @throws {McpError} If `context: 'javascript'` is used.
|
|
92
|
+
*/
|
|
93
|
+
sanitizeString(input, options = {}) {
|
|
94
|
+
if (!input)
|
|
95
|
+
return '';
|
|
96
|
+
// Handle based on context
|
|
97
|
+
switch (options.context) {
|
|
98
|
+
case 'html':
|
|
99
|
+
// Use sanitize-html with custom options
|
|
100
|
+
return this.sanitizeHtml(input, {
|
|
101
|
+
allowedTags: options.allowedTags,
|
|
102
|
+
allowedAttributes: options.allowedAttributes ?
|
|
103
|
+
this.convertAttributesFormat(options.allowedAttributes) :
|
|
104
|
+
undefined
|
|
105
|
+
});
|
|
106
|
+
case 'attribute':
|
|
107
|
+
// Strip HTML tags for attribute context
|
|
108
|
+
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
|
|
109
|
+
case 'url':
|
|
110
|
+
// Validate and sanitize URL
|
|
111
|
+
if (!validator.isURL(input, {
|
|
112
|
+
protocols: ['http', 'https'],
|
|
113
|
+
require_protocol: true
|
|
114
|
+
})) {
|
|
115
|
+
// Return empty string for invalid URLs in this context
|
|
116
|
+
logger.warning('Invalid URL detected during string sanitization', { input });
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
return validator.trim(input);
|
|
120
|
+
case 'javascript':
|
|
121
|
+
// Reject any attempt to sanitize JavaScript
|
|
122
|
+
logger.error('Attempted JavaScript sanitization via sanitizeString', { input: input.substring(0, 50) });
|
|
123
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'JavaScript sanitization not supported through string sanitizer');
|
|
124
|
+
case 'text':
|
|
125
|
+
default:
|
|
126
|
+
// Strip HTML tags for basic text context
|
|
127
|
+
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Sanitize URL with robust validation and sanitization
|
|
132
|
+
* @param input URL to sanitize
|
|
133
|
+
* @param allowedProtocols Allowed URL protocols
|
|
134
|
+
* @returns Sanitized URL
|
|
135
|
+
* @throws {McpError} If URL is invalid
|
|
136
|
+
*/
|
|
137
|
+
sanitizeUrl(input, allowedProtocols = ['http', 'https']) {
|
|
138
|
+
try {
|
|
139
|
+
// First validate the URL format
|
|
140
|
+
if (!validator.isURL(input, {
|
|
141
|
+
protocols: allowedProtocols,
|
|
142
|
+
require_protocol: true
|
|
143
|
+
})) {
|
|
144
|
+
throw new Error('Invalid URL format or protocol');
|
|
145
|
+
}
|
|
146
|
+
// Double-check no javascript: protocol sneaked in
|
|
147
|
+
const lowerInput = input.toLowerCase().trim();
|
|
148
|
+
if (lowerInput.startsWith('javascript:')) {
|
|
149
|
+
throw new Error('JavaScript protocol not allowed');
|
|
150
|
+
}
|
|
151
|
+
// Return the trimmed, validated URL
|
|
152
|
+
return validator.trim(input);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : 'Invalid URL format', { input });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sanitize file paths to prevent path traversal attacks
|
|
160
|
+
* @param input Path to sanitize
|
|
161
|
+
* @param options Options for path sanitization
|
|
162
|
+
* @returns Sanitized and normalized path
|
|
163
|
+
* @throws {McpError} If path is invalid or unsafe
|
|
164
|
+
*/
|
|
165
|
+
sanitizePath(input, options = {}) {
|
|
166
|
+
try {
|
|
167
|
+
if (!input || typeof input !== 'string') {
|
|
168
|
+
throw new Error('Invalid path input: must be a non-empty string');
|
|
169
|
+
}
|
|
170
|
+
// Apply path normalization using built-in path module
|
|
171
|
+
let normalized = path.normalize(input);
|
|
172
|
+
// Prevent null byte injection
|
|
173
|
+
if (normalized.includes('\0')) {
|
|
174
|
+
throw new Error('Path contains null byte');
|
|
175
|
+
}
|
|
176
|
+
// Convert backslashes to forward slashes if toPosix is true
|
|
177
|
+
if (options.toPosix) {
|
|
178
|
+
normalized = normalized.replace(/\\/g, '/');
|
|
179
|
+
}
|
|
180
|
+
// Handle absolute paths based on allowAbsolute option
|
|
181
|
+
if (!options.allowAbsolute && path.isAbsolute(normalized)) {
|
|
182
|
+
// Remove leading slash or drive letter to make it relative
|
|
183
|
+
normalized = normalized.replace(/^(?:[A-Za-z]:)?[/\\]/, '');
|
|
184
|
+
}
|
|
185
|
+
// If rootDir is specified, ensure the path doesn't escape it
|
|
186
|
+
if (options.rootDir) {
|
|
187
|
+
const rootDir = path.resolve(options.rootDir);
|
|
188
|
+
// Resolve the normalized path against the root dir
|
|
189
|
+
const fullPath = path.resolve(rootDir, normalized);
|
|
190
|
+
// More robust check for path traversal: ensure fullPath starts with rootDir + separator
|
|
191
|
+
// or is exactly rootDir
|
|
192
|
+
if (!fullPath.startsWith(rootDir + path.sep) && fullPath !== rootDir) {
|
|
193
|
+
throw new Error('Path traversal detected');
|
|
194
|
+
}
|
|
195
|
+
// Return the path relative to the root
|
|
196
|
+
return path.relative(rootDir, fullPath);
|
|
197
|
+
}
|
|
198
|
+
// Final validation - check for relative path traversal attempts if not rooted
|
|
199
|
+
if (normalized.includes('..')) {
|
|
200
|
+
// Resolve the path to see if it escapes the current working directory conceptually
|
|
201
|
+
const resolvedPath = path.resolve(normalized);
|
|
202
|
+
const currentWorkingDir = path.resolve('.'); // Or use a safer base if needed
|
|
203
|
+
if (!resolvedPath.startsWith(currentWorkingDir)) {
|
|
204
|
+
throw new Error('Relative path traversal detected');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
logger.warning('Path sanitization error', {
|
|
211
|
+
input,
|
|
212
|
+
error: error instanceof Error ? error.message : String(error)
|
|
213
|
+
});
|
|
214
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : 'Invalid or unsafe path', { input });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Sanitize a JSON string
|
|
219
|
+
* @param input JSON string to sanitize
|
|
220
|
+
* @param maxSize Maximum allowed size in bytes
|
|
221
|
+
* @returns Parsed and sanitized object
|
|
222
|
+
* @throws {McpError} If JSON is invalid or too large
|
|
223
|
+
*/
|
|
224
|
+
sanitizeJson(input, maxSize) {
|
|
225
|
+
try {
|
|
226
|
+
if (typeof input !== 'string') {
|
|
227
|
+
throw new Error('Invalid input: expected a JSON string');
|
|
228
|
+
}
|
|
229
|
+
// Check size limit if specified
|
|
230
|
+
if (maxSize !== undefined && Buffer.byteLength(input, 'utf8') > maxSize) {
|
|
231
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `JSON exceeds maximum allowed size of ${maxSize} bytes`, { size: Buffer.byteLength(input, 'utf8'), maxSize });
|
|
232
|
+
}
|
|
233
|
+
// Validate JSON format using JSON.parse for stricter validation than validator.isJSON
|
|
234
|
+
const parsed = JSON.parse(input);
|
|
235
|
+
// Optional: Add recursive sanitization of parsed object values if needed
|
|
236
|
+
// this.sanitizeObjectRecursively(parsed);
|
|
237
|
+
return parsed;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof McpError) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : 'Invalid JSON format', { input: input.length > 100 ? `${input.substring(0, 100)}...` : input });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Ensure input is within a numeric range
|
|
248
|
+
* @param input Number or string to validate
|
|
249
|
+
* @param min Minimum allowed value (inclusive)
|
|
250
|
+
* @param max Maximum allowed value (inclusive)
|
|
251
|
+
* @returns Sanitized number within range
|
|
252
|
+
* @throws {McpError} If input is not a valid number
|
|
253
|
+
*/
|
|
254
|
+
sanitizeNumber(input, min, max) {
|
|
255
|
+
let value;
|
|
256
|
+
// Handle string input
|
|
257
|
+
if (typeof input === 'string') {
|
|
258
|
+
// Use validator for initial check, but rely on parseFloat for conversion
|
|
259
|
+
if (!validator.isNumeric(input.trim())) {
|
|
260
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid number format', { input });
|
|
261
|
+
}
|
|
262
|
+
value = parseFloat(input.trim());
|
|
263
|
+
}
|
|
264
|
+
else if (typeof input === 'number') {
|
|
265
|
+
value = input;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid input type: expected number or string', { input: String(input) });
|
|
269
|
+
}
|
|
270
|
+
// Check if parsing resulted in NaN
|
|
271
|
+
if (isNaN(value) || !isFinite(value)) {
|
|
272
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid number value (NaN or Infinity)', { input });
|
|
273
|
+
}
|
|
274
|
+
// Clamp the value to the specified range
|
|
275
|
+
if (min !== undefined && value < min) {
|
|
276
|
+
value = min;
|
|
277
|
+
logger.debug('Number clamped to minimum value', { input, min, value });
|
|
278
|
+
}
|
|
279
|
+
if (max !== undefined && value > max) {
|
|
280
|
+
value = max;
|
|
281
|
+
logger.debug('Number clamped to maximum value', { input, max, value });
|
|
282
|
+
}
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Sanitize input for logging to protect sensitive information
|
|
287
|
+
* @param input Input to sanitize
|
|
288
|
+
* @returns Sanitized input safe for logging
|
|
289
|
+
*/
|
|
290
|
+
sanitizeForLogging(input) {
|
|
291
|
+
try {
|
|
292
|
+
// Handle non-objects and null directly
|
|
293
|
+
if (!input || typeof input !== 'object') {
|
|
294
|
+
return input;
|
|
295
|
+
}
|
|
296
|
+
// Use structuredClone for deep copy if available (Node.js >= 17)
|
|
297
|
+
// Fallback to JSON stringify/parse for older versions
|
|
298
|
+
const clonedInput = typeof structuredClone === 'function'
|
|
299
|
+
? structuredClone(input)
|
|
300
|
+
: JSON.parse(JSON.stringify(input));
|
|
301
|
+
// Recursively sanitize the cloned object
|
|
302
|
+
this.redactSensitiveFields(clonedInput);
|
|
303
|
+
return clonedInput;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
logger.error('Error during log sanitization', {
|
|
307
|
+
error: error instanceof Error ? error.message : String(error)
|
|
308
|
+
});
|
|
309
|
+
// Return a placeholder if sanitization fails
|
|
310
|
+
return '[Log Sanitization Failed]';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Private helper to convert attribute format from record to sanitize-html format
|
|
315
|
+
*/
|
|
316
|
+
convertAttributesFormat(attrs) {
|
|
317
|
+
// sanitize-html directly supports Record<string, string[]> for allowedAttributes per tag
|
|
318
|
+
return attrs;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Recursively redact sensitive fields in an object or array
|
|
322
|
+
*/
|
|
323
|
+
redactSensitiveFields(obj) {
|
|
324
|
+
if (!obj || typeof obj !== 'object') {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Handle arrays: iterate and recurse
|
|
328
|
+
if (Array.isArray(obj)) {
|
|
329
|
+
obj.forEach((item, index) => {
|
|
330
|
+
// If the item is an object/array, recurse. Otherwise, leave primitive values.
|
|
331
|
+
if (item && typeof item === 'object') {
|
|
332
|
+
this.redactSensitiveFields(item);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Handle regular objects: iterate through keys
|
|
338
|
+
for (const key in obj) {
|
|
339
|
+
// Use hasOwnProperty to avoid iterating over prototype properties
|
|
340
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
341
|
+
const value = obj[key];
|
|
342
|
+
// Check if this key matches any sensitive field pattern (case-insensitive)
|
|
343
|
+
const isSensitive = this.sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
|
|
344
|
+
if (isSensitive) {
|
|
345
|
+
// Mask sensitive value
|
|
346
|
+
obj[key] = '[REDACTED]';
|
|
347
|
+
}
|
|
348
|
+
else if (value && typeof value === 'object') {
|
|
349
|
+
// Recursively process nested objects/arrays
|
|
350
|
+
this.redactSensitiveFields(value);
|
|
351
|
+
}
|
|
352
|
+
// Primitive values are left as is if not sensitive
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Create and export singleton instance
|
|
358
|
+
export const sanitization = Sanitization.getInstance();
|
|
359
|
+
// Removed the `sanitizeInput` object export for simplicity.
|
|
360
|
+
// Users should import `sanitization` and call methods directly.
|
|
361
|
+
// e.g., import { sanitization } from './sanitization.js';
|
|
362
|
+
// sanitization.sanitizeHtml(input);
|
|
363
|
+
// sanitization.sanitizePath(input);
|
|
364
|
+
/**
|
|
365
|
+
* Sanitize input for logging to protect sensitive information.
|
|
366
|
+
* Kept as a separate export for convenience.
|
|
367
|
+
* @param input Input to sanitize
|
|
368
|
+
* @returns Sanitized input safe for logging
|
|
369
|
+
*/
|
|
370
|
+
export const sanitizeInputForLogging = (input) => sanitization.sanitizeForLogging(input);
|
|
371
|
+
// Removed default export
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { encoding_for_model } from 'tiktoken';
|
|
2
|
+
import { BaseErrorCode } from '../types-global/errors.js'; // Import BaseErrorCode and McpError
|
|
3
|
+
import { ErrorHandler } from './errorHandler.js'; // Import ErrorHandler
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
// Define the model used specifically for token counting
|
|
6
|
+
const TOKENIZATION_MODEL = 'gpt-4o'; // Note this is strictly for token counting, not the model used for inference
|
|
7
|
+
/**
|
|
8
|
+
* Calculates the number of tokens for a given text using the 'gpt-4o' tokenizer.
|
|
9
|
+
* Uses ErrorHandler for consistent error management.
|
|
10
|
+
*
|
|
11
|
+
* @param text - The input text to tokenize.
|
|
12
|
+
* @param context - Optional request context for logging and error handling.
|
|
13
|
+
* @returns The number of tokens.
|
|
14
|
+
* @throws {McpError} Throws an McpError if tokenization fails.
|
|
15
|
+
*/
|
|
16
|
+
export async function countTokens(text, context) {
|
|
17
|
+
// Wrap the synchronous operation in tryCatch which handles both sync/async
|
|
18
|
+
return ErrorHandler.tryCatch(() => {
|
|
19
|
+
let encoding = null;
|
|
20
|
+
try {
|
|
21
|
+
// Always use the defined TOKENIZATION_MODEL
|
|
22
|
+
encoding = encoding_for_model(TOKENIZATION_MODEL);
|
|
23
|
+
const tokens = encoding.encode(text);
|
|
24
|
+
return tokens.length;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
encoding?.free(); // Ensure the encoder is freed if it was successfully created
|
|
28
|
+
}
|
|
29
|
+
}, {
|
|
30
|
+
operation: 'countTokens',
|
|
31
|
+
context: context,
|
|
32
|
+
input: { textSample: text.substring(0, 50) + '...' }, // Log sanitized input
|
|
33
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR for external lib issues
|
|
34
|
+
rethrow: true // Rethrow as McpError
|
|
35
|
+
// Removed onErrorReturn as we now rethrow
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculates the number of tokens for chat messages using the ChatCompletionMessageParam structure
|
|
40
|
+
* and the 'gpt-4o' tokenizer, considering special tokens and message overhead.
|
|
41
|
+
* This implementation is based on OpenAI's guidelines for gpt-4/gpt-3.5-turbo models.
|
|
42
|
+
* Uses ErrorHandler for consistent error management.
|
|
43
|
+
*
|
|
44
|
+
* See: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
|
45
|
+
*
|
|
46
|
+
* @param messages - An array of chat messages in the `ChatCompletionMessageParam` format.
|
|
47
|
+
* @param context - Optional request context for logging and error handling.
|
|
48
|
+
* @returns The estimated number of tokens.
|
|
49
|
+
* @throws {McpError} Throws an McpError if tokenization fails.
|
|
50
|
+
*/
|
|
51
|
+
export async function countChatTokens(messages, // Use the complex type
|
|
52
|
+
context) {
|
|
53
|
+
// Wrap the synchronous operation in tryCatch
|
|
54
|
+
return ErrorHandler.tryCatch(() => {
|
|
55
|
+
let encoding = null;
|
|
56
|
+
let num_tokens = 0;
|
|
57
|
+
try {
|
|
58
|
+
// Always use the defined TOKENIZATION_MODEL
|
|
59
|
+
encoding = encoding_for_model(TOKENIZATION_MODEL);
|
|
60
|
+
// Define tokens per message/name based on gpt-4o (same as gpt-4/gpt-3.5-turbo)
|
|
61
|
+
const tokens_per_message = 3;
|
|
62
|
+
const tokens_per_name = 1;
|
|
63
|
+
for (const message of messages) {
|
|
64
|
+
num_tokens += tokens_per_message;
|
|
65
|
+
// Encode role
|
|
66
|
+
num_tokens += encoding.encode(message.role).length;
|
|
67
|
+
// Encode content - handle potential null or array content (vision)
|
|
68
|
+
if (typeof message.content === 'string') {
|
|
69
|
+
num_tokens += encoding.encode(message.content).length;
|
|
70
|
+
}
|
|
71
|
+
else if (Array.isArray(message.content)) {
|
|
72
|
+
// Handle multi-part content (e.g., text + image) - simplified: encode text parts only
|
|
73
|
+
for (const part of message.content) {
|
|
74
|
+
if (part.type === 'text') {
|
|
75
|
+
num_tokens += encoding.encode(part.text).length;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Add placeholder token count for non-text parts (e.g., images) if needed
|
|
79
|
+
// This requires specific model knowledge (e.g., OpenAI vision model token costs)
|
|
80
|
+
logger.warning(`Non-text content part found (type: ${part.type}), token count contribution ignored.`, context);
|
|
81
|
+
// num_tokens += IMAGE_TOKEN_COST; // Placeholder
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} // else: content is null, add 0 tokens
|
|
85
|
+
// Encode name if present (often associated with 'tool' or 'function' roles in newer models)
|
|
86
|
+
if ('name' in message && message.name) {
|
|
87
|
+
num_tokens += tokens_per_name;
|
|
88
|
+
num_tokens += encoding.encode(message.name).length;
|
|
89
|
+
}
|
|
90
|
+
// --- Handle tool calls (specific to newer models) ---
|
|
91
|
+
// Assistant message requesting tool calls
|
|
92
|
+
if (message.role === 'assistant' && 'tool_calls' in message && message.tool_calls) {
|
|
93
|
+
for (const tool_call of message.tool_calls) {
|
|
94
|
+
// Add tokens for the function name and arguments
|
|
95
|
+
if (tool_call.function.name) {
|
|
96
|
+
num_tokens += encoding.encode(tool_call.function.name).length;
|
|
97
|
+
}
|
|
98
|
+
if (tool_call.function.arguments) {
|
|
99
|
+
// Arguments are often JSON strings
|
|
100
|
+
num_tokens += encoding.encode(tool_call.function.arguments).length;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Tool message providing results
|
|
105
|
+
if (message.role === 'tool' && 'tool_call_id' in message && message.tool_call_id) {
|
|
106
|
+
num_tokens += encoding.encode(message.tool_call_id).length;
|
|
107
|
+
// Content of the tool message (the result) is already handled by the string content check above
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
num_tokens += 3; // every reply is primed with <|start|>assistant<|message|>
|
|
111
|
+
return num_tokens;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
encoding?.free();
|
|
115
|
+
}
|
|
116
|
+
}, {
|
|
117
|
+
operation: 'countChatTokens',
|
|
118
|
+
context: context,
|
|
119
|
+
input: { messageCount: messages.length }, // Log sanitized input
|
|
120
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR
|
|
121
|
+
rethrow: true // Rethrow as McpError
|
|
122
|
+
// Removed onErrorReturn
|
|
123
|
+
});
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/git-mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "An MCP (Model Context Protocol) server providing tools to interact with Git repositories. Enables LLMs and AI agents to perform Git operations like clone, commit, push, pull, branch, diff, log, status, and more via the MCP standard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Casey Hand @cyanheads",
|
|
@@ -9,31 +9,76 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/cyanheads/git-mcp-server"
|
|
11
11
|
},
|
|
12
|
+
"homepage": "https://github.com/cyanheads/git-mcp-server#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/cyanheads/git-mcp-server/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
12
19
|
"bin": {
|
|
13
|
-
"git-mcp-server": "./
|
|
20
|
+
"git-mcp-server": "./dist/index.js"
|
|
14
21
|
},
|
|
15
22
|
"files": [
|
|
16
|
-
"
|
|
23
|
+
"dist"
|
|
17
24
|
],
|
|
18
25
|
"scripts": {
|
|
19
|
-
"build": "tsc && node -
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"tree": "ts-node scripts/tree.ts",
|
|
25
|
-
"
|
|
26
|
+
"build": "tsc && node --loader ts-node/esm scripts/make-executable.ts dist/index.js",
|
|
27
|
+
"start": "node dist/index.js",
|
|
28
|
+
"start:stdio": "MCP_TRANSPORT_TYPE=stdio node dist/index.js",
|
|
29
|
+
"start:http": "MCP_TRANSPORT_TYPE=http node dist/index.js",
|
|
30
|
+
"rebuild": "ts-node --esm scripts/clean.ts && npm run build",
|
|
31
|
+
"tree": "ts-node --esm scripts/tree.ts",
|
|
32
|
+
"inspector": "npx @modelcontextprotocol/inspector dist/index.js",
|
|
33
|
+
"clean": "ts-node --esm scripts/clean.ts"
|
|
26
34
|
},
|
|
27
35
|
"publishConfig": {
|
|
28
36
|
"access": "public"
|
|
29
37
|
},
|
|
30
38
|
"dependencies": {
|
|
31
|
-
"@modelcontextprotocol/sdk": "1.
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"@types/
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.10.2",
|
|
40
|
+
"@types/node": "^22.15.3",
|
|
41
|
+
"@types/sanitize-html": "^2.15.0",
|
|
42
|
+
"@types/validator": "13.15.0",
|
|
43
|
+
"dotenv": "^16.5.0",
|
|
44
|
+
"express": "^5.1.0",
|
|
45
|
+
"ignore": "^7.0.4",
|
|
46
|
+
"openai": "^4.96.2",
|
|
47
|
+
"partial-json": "^0.1.7",
|
|
48
|
+
"sanitize-html": "^2.16.0",
|
|
49
|
+
"tiktoken": "^1.0.21",
|
|
35
50
|
"ts-node": "^10.9.2",
|
|
36
|
-
"typescript": "^5.8.
|
|
37
|
-
"
|
|
51
|
+
"typescript": "^5.8.3",
|
|
52
|
+
"validator": "13.15.0",
|
|
53
|
+
"winston": "^3.17.0",
|
|
54
|
+
"winston-daily-rotate-file": "^5.0.0",
|
|
55
|
+
"yargs": "^17.7.2",
|
|
56
|
+
"zod": "^3.24.3"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"typescript",
|
|
60
|
+
"MCP",
|
|
61
|
+
"model-context-protocol",
|
|
62
|
+
"LLM",
|
|
63
|
+
"AI-integration",
|
|
64
|
+
"server",
|
|
65
|
+
"git",
|
|
66
|
+
"version-control",
|
|
67
|
+
"repository",
|
|
68
|
+
"commit",
|
|
69
|
+
"branch",
|
|
70
|
+
"diff",
|
|
71
|
+
"log",
|
|
72
|
+
"status",
|
|
73
|
+
"push",
|
|
74
|
+
"pull",
|
|
75
|
+
"clone",
|
|
76
|
+
"automation",
|
|
77
|
+
"devops",
|
|
78
|
+
"ai-agent",
|
|
79
|
+
"llm-tools"
|
|
80
|
+
],
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@types/express": "^5.0.1"
|
|
38
83
|
}
|
|
39
84
|
}
|
package/build/index.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Git MCP Server Entry Point
|
|
4
|
-
* =========================
|
|
5
|
-
*
|
|
6
|
-
* This is the main entry point for the Git MCP server.
|
|
7
|
-
* It creates a server instance and connects it to a stdio transport.
|
|
8
|
-
*/
|
|
9
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
import { GitMcpServer } from './server.js';
|
|
11
|
-
import dotenv from 'dotenv';
|
|
12
|
-
// Load environment variables from .env file if it exists
|
|
13
|
-
dotenv.config();
|
|
14
|
-
/**
|
|
15
|
-
* Main function to start the server
|
|
16
|
-
*/
|
|
17
|
-
async function main() {
|
|
18
|
-
console.error('Starting Git MCP Server...');
|
|
19
|
-
try {
|
|
20
|
-
// Create server instance
|
|
21
|
-
const server = new GitMcpServer();
|
|
22
|
-
// Use stdio transport for communication
|
|
23
|
-
const transport = new StdioServerTransport();
|
|
24
|
-
// Connect the server to the transport
|
|
25
|
-
await server.connect(transport);
|
|
26
|
-
console.error('Git MCP Server running on stdio transport');
|
|
27
|
-
// Handle interruption signals
|
|
28
|
-
process.on('SIGINT', async () => {
|
|
29
|
-
console.error('Received SIGINT, shutting down Git MCP Server');
|
|
30
|
-
process.exit(0);
|
|
31
|
-
});
|
|
32
|
-
process.on('SIGTERM', async () => {
|
|
33
|
-
console.error('Received SIGTERM, shutting down Git MCP Server');
|
|
34
|
-
process.exit(0);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
console.error('Failed to start Git MCP Server:');
|
|
39
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
40
|
-
if (error instanceof Error && error.stack) {
|
|
41
|
-
console.error(error.stack);
|
|
42
|
-
}
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// Start the server
|
|
47
|
-
main().catch((error) => {
|
|
48
|
-
console.error('Unhandled error in main process:');
|
|
49
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
50
|
-
if (error instanceof Error && error.stack) {
|
|
51
|
-
console.error(error.stack);
|
|
52
|
-
}
|
|
53
|
-
process.exit(1);
|
|
54
|
-
});
|