@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.
Files changed (105) hide show
  1. package/README.md +172 -285
  2. package/dist/config/index.js +69 -0
  3. package/dist/index.js +135 -0
  4. package/dist/mcp-server/server.js +572 -0
  5. package/dist/mcp-server/tools/gitAdd/index.js +7 -0
  6. package/dist/mcp-server/tools/gitAdd/logic.js +118 -0
  7. package/dist/mcp-server/tools/gitAdd/registration.js +73 -0
  8. package/dist/mcp-server/tools/gitBranch/index.js +7 -0
  9. package/dist/mcp-server/tools/gitBranch/logic.js +180 -0
  10. package/dist/mcp-server/tools/gitBranch/registration.js +72 -0
  11. package/dist/mcp-server/tools/gitCheckout/index.js +6 -0
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +165 -0
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +78 -0
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +7 -0
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +115 -0
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +69 -0
  17. package/dist/mcp-server/tools/gitClean/index.js +7 -0
  18. package/dist/mcp-server/tools/gitClean/logic.js +110 -0
  19. package/dist/mcp-server/tools/gitClean/registration.js +98 -0
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +7 -0
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +35 -0
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +73 -0
  23. package/dist/mcp-server/tools/gitClone/index.js +7 -0
  24. package/dist/mcp-server/tools/gitClone/logic.js +136 -0
  25. package/dist/mcp-server/tools/gitClone/registration.js +44 -0
  26. package/dist/mcp-server/tools/gitCommit/index.js +7 -0
  27. package/dist/mcp-server/tools/gitCommit/logic.js +129 -0
  28. package/dist/mcp-server/tools/gitCommit/registration.js +100 -0
  29. package/dist/mcp-server/tools/gitDiff/index.js +6 -0
  30. package/dist/mcp-server/tools/gitDiff/logic.js +114 -0
  31. package/dist/mcp-server/tools/gitDiff/registration.js +74 -0
  32. package/dist/mcp-server/tools/gitFetch/index.js +6 -0
  33. package/dist/mcp-server/tools/gitFetch/logic.js +116 -0
  34. package/dist/mcp-server/tools/gitFetch/registration.js +71 -0
  35. package/dist/mcp-server/tools/gitInit/index.js +7 -0
  36. package/dist/mcp-server/tools/gitInit/logic.js +117 -0
  37. package/dist/mcp-server/tools/gitInit/registration.js +44 -0
  38. package/dist/mcp-server/tools/gitLog/index.js +6 -0
  39. package/dist/mcp-server/tools/gitLog/logic.js +148 -0
  40. package/dist/mcp-server/tools/gitLog/registration.js +71 -0
  41. package/dist/mcp-server/tools/gitMerge/index.js +7 -0
  42. package/dist/mcp-server/tools/gitMerge/logic.js +160 -0
  43. package/dist/mcp-server/tools/gitMerge/registration.js +77 -0
  44. package/dist/mcp-server/tools/gitPull/index.js +6 -0
  45. package/dist/mcp-server/tools/gitPull/logic.js +144 -0
  46. package/dist/mcp-server/tools/gitPull/registration.js +81 -0
  47. package/dist/mcp-server/tools/gitPush/index.js +6 -0
  48. package/dist/mcp-server/tools/gitPush/logic.js +188 -0
  49. package/dist/mcp-server/tools/gitPush/registration.js +81 -0
  50. package/dist/mcp-server/tools/gitRebase/index.js +7 -0
  51. package/dist/mcp-server/tools/gitRebase/logic.js +171 -0
  52. package/dist/mcp-server/tools/gitRebase/registration.js +72 -0
  53. package/dist/mcp-server/tools/gitRemote/index.js +7 -0
  54. package/dist/mcp-server/tools/gitRemote/logic.js +158 -0
  55. package/dist/mcp-server/tools/gitRemote/registration.js +76 -0
  56. package/dist/mcp-server/tools/gitReset/index.js +6 -0
  57. package/dist/mcp-server/tools/gitReset/logic.js +116 -0
  58. package/dist/mcp-server/tools/gitReset/registration.js +71 -0
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +7 -0
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +91 -0
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +78 -0
  62. package/dist/mcp-server/tools/gitShow/index.js +7 -0
  63. package/dist/mcp-server/tools/gitShow/logic.js +99 -0
  64. package/dist/mcp-server/tools/gitShow/registration.js +83 -0
  65. package/dist/mcp-server/tools/gitStash/index.js +7 -0
  66. package/dist/mcp-server/tools/gitStash/logic.js +161 -0
  67. package/dist/mcp-server/tools/gitStash/registration.js +84 -0
  68. package/dist/mcp-server/tools/gitStatus/index.js +7 -0
  69. package/dist/mcp-server/tools/gitStatus/logic.js +215 -0
  70. package/dist/mcp-server/tools/gitStatus/registration.js +77 -0
  71. package/dist/mcp-server/tools/gitTag/index.js +7 -0
  72. package/dist/mcp-server/tools/gitTag/logic.js +142 -0
  73. package/dist/mcp-server/tools/gitTag/registration.js +84 -0
  74. package/dist/types-global/errors.js +68 -0
  75. package/dist/types-global/mcp.js +59 -0
  76. package/dist/types-global/tool.js +1 -0
  77. package/dist/utils/errorHandler.js +237 -0
  78. package/dist/utils/idGenerator.js +148 -0
  79. package/dist/utils/index.js +11 -0
  80. package/dist/utils/jsonParser.js +78 -0
  81. package/dist/utils/logger.js +266 -0
  82. package/dist/utils/rateLimiter.js +177 -0
  83. package/dist/utils/requestContext.js +49 -0
  84. package/dist/utils/sanitization.js +371 -0
  85. package/dist/utils/tokenCounter.js +124 -0
  86. package/package.json +62 -17
  87. package/build/index.js +0 -54
  88. package/build/resources/descriptors.js +0 -77
  89. package/build/resources/diff.js +0 -241
  90. package/build/resources/file.js +0 -222
  91. package/build/resources/history.js +0 -242
  92. package/build/resources/index.js +0 -99
  93. package/build/resources/repository.js +0 -286
  94. package/build/server.js +0 -120
  95. package/build/services/error-service.js +0 -73
  96. package/build/services/git-service.js +0 -965
  97. package/build/tools/advanced.js +0 -526
  98. package/build/tools/branch.js +0 -296
  99. package/build/tools/index.js +0 -29
  100. package/build/tools/remote.js +0 -279
  101. package/build/tools/repository.js +0 -170
  102. package/build/tools/workdir.js +0 -445
  103. package/build/types/git.js +0 -7
  104. package/build/utils/global-settings.js +0 -64
  105. 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": "1.2.4",
4
- "description": "A Model Context Protocol server for Git integration",
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": "./build/index.js"
20
+ "git-mcp-server": "./dist/index.js"
14
21
  },
15
22
  "files": [
16
- "build"
23
+ "dist"
17
24
  ],
18
25
  "scripts": {
19
- "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
20
- "prepare": "npm run build",
21
- "watch": "tsc --watch",
22
- "inspector": "npx @modelcontextprotocol/inspector build/index.js",
23
- "clean": "ts-node scripts/clean.ts",
24
- "tree": "ts-node scripts/tree.ts",
25
- "rebuild": "npm run clean && npm run build"
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.8.0",
32
- "dotenv": "^16.4.7",
33
- "zod": "^3.24.2",
34
- "@types/node": "^22.13.17",
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.2",
37
- "simple-git": "^3.27.0"
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
- });