@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,237 @@
1
+ import { BaseErrorCode, McpError } from '../types-global/errors.js';
2
+ import { logger } from './logger.js';
3
+ import { sanitizeInputForLogging } from './sanitization.js'; // Updated import
4
+ /**
5
+ * Simple mapper that maps error types to error codes
6
+ */
7
+ const ERROR_TYPE_MAPPINGS = {
8
+ 'SyntaxError': BaseErrorCode.VALIDATION_ERROR,
9
+ 'TypeError': BaseErrorCode.VALIDATION_ERROR,
10
+ 'ReferenceError': BaseErrorCode.INTERNAL_ERROR,
11
+ 'RangeError': BaseErrorCode.VALIDATION_ERROR,
12
+ 'URIError': BaseErrorCode.VALIDATION_ERROR,
13
+ 'EvalError': BaseErrorCode.INTERNAL_ERROR
14
+ };
15
+ /**
16
+ * Common error patterns for automatic classification
17
+ */
18
+ const COMMON_ERROR_PATTERNS = [
19
+ // Authentication related errors
20
+ { pattern: /auth|unauthorized|unauthenticated|not.*logged.*in|invalid.*token|expired.*token/i, errorCode: BaseErrorCode.UNAUTHORIZED },
21
+ // Permission related errors
22
+ { pattern: /permission|forbidden|access.*denied|not.*allowed/i, errorCode: BaseErrorCode.FORBIDDEN },
23
+ // Not found errors
24
+ { pattern: /not.*found|missing|no.*such|doesn't.*exist|couldn't.*find/i, errorCode: BaseErrorCode.NOT_FOUND },
25
+ // Validation errors
26
+ { pattern: /invalid|validation|malformed|bad request|wrong format/i, errorCode: BaseErrorCode.VALIDATION_ERROR },
27
+ // Conflict errors
28
+ { pattern: /conflict|already.*exists|duplicate|unique.*constraint/i, errorCode: BaseErrorCode.CONFLICT },
29
+ // Rate limiting
30
+ { pattern: /rate.*limit|too.*many.*requests|throttled/i, errorCode: BaseErrorCode.RATE_LIMITED },
31
+ // Timeout errors
32
+ { pattern: /timeout|timed.*out|deadline.*exceeded/i, errorCode: BaseErrorCode.TIMEOUT },
33
+ // External service errors
34
+ { pattern: /service.*unavailable|bad.*gateway|gateway.*timeout/i, errorCode: BaseErrorCode.SERVICE_UNAVAILABLE }
35
+ ];
36
+ /**
37
+ * Get a readable name for an error
38
+ * @param error Error to get name for
39
+ * @returns User-friendly error name
40
+ */
41
+ function getErrorName(error) {
42
+ if (error instanceof Error) {
43
+ return error.name || 'Error';
44
+ }
45
+ if (error === null) {
46
+ return 'NullError';
47
+ }
48
+ if (error === undefined) {
49
+ return 'UndefinedError';
50
+ }
51
+ return typeof error === 'object'
52
+ ? 'ObjectError'
53
+ : 'UnknownError';
54
+ }
55
+ /**
56
+ * Get a message from an error
57
+ * @param error Error to get message from
58
+ * @returns Error message
59
+ */
60
+ function getErrorMessage(error) {
61
+ if (error instanceof Error) {
62
+ return error.message;
63
+ }
64
+ if (error === null) {
65
+ return 'Null error occurred';
66
+ }
67
+ if (error === undefined) {
68
+ return 'Undefined error occurred';
69
+ }
70
+ return typeof error === 'string'
71
+ ? error
72
+ : String(error);
73
+ }
74
+ /**
75
+ * Error handler utility class with various error handling methods
76
+ */
77
+ export class ErrorHandler {
78
+ /**
79
+ * Determine the appropriate error code for an error based on patterns and type
80
+ * @param error The error to classify
81
+ * @returns The appropriate error code
82
+ */
83
+ static determineErrorCode(error) {
84
+ // If it's already an McpError, use its code
85
+ if (error instanceof McpError) {
86
+ return error.code;
87
+ }
88
+ const errorName = getErrorName(error);
89
+ const errorMessage = getErrorMessage(error);
90
+ // Check if the error type has a direct mapping
91
+ if (errorName in ERROR_TYPE_MAPPINGS) {
92
+ return ERROR_TYPE_MAPPINGS[errorName];
93
+ }
94
+ // Check for common error patterns
95
+ for (const pattern of COMMON_ERROR_PATTERNS) {
96
+ const regex = pattern.pattern instanceof RegExp
97
+ ? pattern.pattern
98
+ : new RegExp(pattern.pattern, 'i');
99
+ if (regex.test(errorMessage) || regex.test(errorName)) {
100
+ return pattern.errorCode;
101
+ }
102
+ }
103
+ // Default to internal error if no pattern matches
104
+ return BaseErrorCode.INTERNAL_ERROR;
105
+ }
106
+ /**
107
+ * Handle operation errors with consistent logging and transformation
108
+ * @param error The error that occurred
109
+ * @param options Error handling options
110
+ * @returns The transformed error
111
+ */
112
+ static handleError(error, options) {
113
+ const { context, operation, input, rethrow = false, errorCode: explicitErrorCode, includeStack = true, critical = false } = options;
114
+ // If it's already an McpError, use it directly but apply additional context
115
+ if (error instanceof McpError) {
116
+ // Add any additional context
117
+ if (context && Object.keys(context).length > 0) {
118
+ error.details = { ...error.details, ...context };
119
+ }
120
+ // Log the error with sanitized input
121
+ logger.error(`Error ${operation}: ${error.message}`, {
122
+ errorCode: error.code,
123
+ requestId: context?.requestId,
124
+ input: input ? sanitizeInputForLogging(input) : undefined,
125
+ stack: includeStack ? error.stack : undefined,
126
+ critical,
127
+ ...context
128
+ });
129
+ if (rethrow) {
130
+ throw error;
131
+ }
132
+ return error;
133
+ }
134
+ // Sanitize input for logging
135
+ const sanitizedInput = input ? sanitizeInputForLogging(input) : undefined;
136
+ // Log the error with consistent format
137
+ logger.error(`Error ${operation}`, {
138
+ error: error instanceof Error ? error.message : String(error),
139
+ errorType: getErrorName(error),
140
+ input: sanitizedInput,
141
+ requestId: context?.requestId,
142
+ stack: includeStack && error instanceof Error ? error.stack : undefined,
143
+ critical,
144
+ ...context
145
+ });
146
+ // Choose the error code (explicit > determined > default)
147
+ const errorCode = explicitErrorCode ||
148
+ ErrorHandler.determineErrorCode(error) ||
149
+ BaseErrorCode.INTERNAL_ERROR;
150
+ // Transform to appropriate error type
151
+ const transformedError = options.errorMapper
152
+ ? options.errorMapper(error)
153
+ : new McpError(errorCode, `Error ${operation}: ${error instanceof Error ? error.message : 'Unknown error'}`, {
154
+ originalError: getErrorName(error),
155
+ ...context
156
+ });
157
+ // Rethrow if requested
158
+ if (rethrow) {
159
+ throw transformedError;
160
+ }
161
+ return transformedError;
162
+ }
163
+ /**
164
+ * Map an error to a specific error type based on error message patterns
165
+ * @param error The error to map
166
+ * @param mappings Array of pattern and factory mappings
167
+ * @param defaultFactory Default factory function if no pattern matches
168
+ * @returns The mapped error
169
+ */
170
+ static mapError(error, mappings, defaultFactory) {
171
+ // If it's already the target type and we have a default factory to check against, return it
172
+ if (defaultFactory && error instanceof Error) {
173
+ const defaultInstance = defaultFactory(error);
174
+ if (error.constructor === defaultInstance.constructor) {
175
+ return error;
176
+ }
177
+ }
178
+ const errorMessage = getErrorMessage(error);
179
+ // Check each pattern and return the first match
180
+ for (const mapping of mappings) {
181
+ const matches = mapping.pattern instanceof RegExp
182
+ ? mapping.pattern.test(errorMessage)
183
+ : errorMessage.includes(mapping.pattern);
184
+ if (matches) {
185
+ return mapping.factory(error, mapping.additionalContext);
186
+ }
187
+ }
188
+ // Return default or original error
189
+ if (defaultFactory) {
190
+ return defaultFactory(error);
191
+ }
192
+ return error instanceof Error
193
+ ? error
194
+ : new Error(String(error));
195
+ }
196
+ // Removed createErrorMapper method for simplification
197
+ /**
198
+ * Format an error for consistent response structure
199
+ * @param error The error to format
200
+ * @returns Formatted error object
201
+ */
202
+ static formatError(error) {
203
+ if (error instanceof McpError) {
204
+ return {
205
+ code: error.code,
206
+ message: error.message,
207
+ details: error.details || {}
208
+ };
209
+ }
210
+ if (error instanceof Error) {
211
+ return {
212
+ code: ErrorHandler.determineErrorCode(error),
213
+ message: error.message,
214
+ details: { errorType: error.name }
215
+ };
216
+ }
217
+ return {
218
+ code: BaseErrorCode.UNKNOWN_ERROR,
219
+ message: String(error),
220
+ details: { errorType: typeof error }
221
+ };
222
+ }
223
+ /**
224
+ * Safely execute a function and handle any errors
225
+ * @param fn Function to execute
226
+ * @param options Error handling options
227
+ * @returns The result of the function or error
228
+ */
229
+ static async tryCatch(fn, options) {
230
+ try {
231
+ return await fn();
232
+ }
233
+ catch (error) {
234
+ throw ErrorHandler.handleError(error, { ...options, rethrow: true });
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,148 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { BaseErrorCode, McpError } from '../types-global/errors.js';
3
+ import { logger } from './logger.js';
4
+ /**
5
+ * Generic ID Generator class for creating and managing unique identifiers
6
+ */
7
+ export class IdGenerator {
8
+ // Default charset
9
+ static DEFAULT_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
10
+ // Default separator
11
+ static DEFAULT_SEPARATOR = '_';
12
+ // Default random part length
13
+ static DEFAULT_LENGTH = 6;
14
+ // Entity prefixes
15
+ entityPrefixes = {};
16
+ // Reverse mapping for prefix to entity type lookup
17
+ prefixToEntityType = {};
18
+ /**
19
+ * Constructor that accepts entity prefix configuration
20
+ * @param entityPrefixes Map of entity types to their prefixes
21
+ */
22
+ constructor(entityPrefixes = {}) {
23
+ this.setEntityPrefixes(entityPrefixes);
24
+ }
25
+ /**
26
+ * Set or update entity prefixes and rebuild the reverse lookup
27
+ * @param entityPrefixes Map of entity types to their prefixes
28
+ */
29
+ setEntityPrefixes(entityPrefixes) {
30
+ this.entityPrefixes = { ...entityPrefixes };
31
+ // Rebuild reverse mapping
32
+ this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce((acc, [type, prefix]) => {
33
+ acc[prefix] = type;
34
+ acc[prefix.toLowerCase()] = type;
35
+ return acc;
36
+ }, {});
37
+ logger.debug('Entity prefixes updated', { entityPrefixes: this.entityPrefixes });
38
+ }
39
+ /**
40
+ * Get all registered entity prefixes
41
+ * @returns The entity prefix configuration
42
+ */
43
+ getEntityPrefixes() {
44
+ return { ...this.entityPrefixes };
45
+ }
46
+ /**
47
+ * Generates a cryptographically secure random alphanumeric string
48
+ * @param length The length of the random string to generate
49
+ * @param charset Optional custom character set
50
+ * @returns Random alphanumeric string
51
+ */
52
+ generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) {
53
+ const bytes = randomBytes(length);
54
+ let result = '';
55
+ for (let i = 0; i < length; i++) {
56
+ result += charset[bytes[i] % charset.length];
57
+ }
58
+ return result;
59
+ }
60
+ /**
61
+ * Generates a unique ID with an optional prefix
62
+ * @param prefix Optional prefix to add to the ID
63
+ * @param options Optional generation options
64
+ * @returns A unique identifier string
65
+ */
66
+ generate(prefix, options = {}) {
67
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET } = options;
68
+ const randomPart = this.generateRandomString(length, charset);
69
+ return prefix
70
+ ? `${prefix}${separator}${randomPart}`
71
+ : randomPart;
72
+ }
73
+ /**
74
+ * Generates a custom ID for an entity with format PREFIX_XXXXXX
75
+ * @param entityType The type of entity to generate an ID for
76
+ * @param options Optional generation options
77
+ * @returns A unique identifier string (e.g., "PROJ_A6B3J0")
78
+ * @throws {McpError} If the entity type is not registered
79
+ */
80
+ generateForEntity(entityType, options = {}) {
81
+ const prefix = this.entityPrefixes[entityType];
82
+ if (!prefix) {
83
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type: ${entityType}`);
84
+ }
85
+ return this.generate(prefix, options);
86
+ }
87
+ /**
88
+ * Validates if a given ID matches the expected format for an entity type
89
+ * @param id The ID to validate
90
+ * @param entityType The expected entity type
91
+ * @param options Optional validation options
92
+ * @returns boolean indicating if the ID is valid
93
+ */
94
+ isValid(id, entityType, options = {}) {
95
+ const prefix = this.entityPrefixes[entityType];
96
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR } = options;
97
+ if (!prefix) {
98
+ return false;
99
+ }
100
+ const pattern = new RegExp(`^${prefix}${separator}[A-Z0-9]{${length}}$`);
101
+ return pattern.test(id);
102
+ }
103
+ /**
104
+ * Strips the prefix from an ID
105
+ * @param id The ID to strip
106
+ * @param separator Optional custom separator
107
+ * @returns The ID without the prefix
108
+ */
109
+ stripPrefix(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
110
+ return id.split(separator)[1] || id;
111
+ }
112
+ /**
113
+ * Determines the entity type from an ID
114
+ * @param id The ID to get the entity type for
115
+ * @param separator Optional custom separator
116
+ * @returns The entity type
117
+ * @throws {McpError} If the ID format is invalid or entity type is unknown
118
+ */
119
+ getEntityType(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
120
+ const parts = id.split(separator);
121
+ if (parts.length !== 2 || !parts[0]) {
122
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid ID format: ${id}. Expected format: PREFIX${separator}XXXXXX`);
123
+ }
124
+ const prefix = parts[0];
125
+ const entityType = this.prefixToEntityType[prefix];
126
+ if (!entityType) {
127
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type prefix: ${prefix}`);
128
+ }
129
+ return entityType;
130
+ }
131
+ /**
132
+ * Normalizes an entity ID to ensure consistent uppercase format
133
+ * @param id The ID to normalize
134
+ * @param separator Optional custom separator
135
+ * @returns The normalized ID in uppercase format
136
+ */
137
+ normalize(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
138
+ const entityType = this.getEntityType(id, separator);
139
+ const idParts = id.split(separator);
140
+ return `${this.entityPrefixes[entityType]}${separator}${idParts[1].toUpperCase()}`;
141
+ }
142
+ }
143
+ // Create and export a default instance with an empty entity prefix configuration
144
+ export const idGenerator = new IdGenerator();
145
+ // For standalone use as a UUID generator
146
+ export const generateUUID = () => {
147
+ return crypto.randomUUID();
148
+ };
@@ -0,0 +1,11 @@
1
+ // Re-export all utilities using wildcard exports for simplicity
2
+ export * from './requestContext.js';
3
+ export * from './errorHandler.js';
4
+ export * from './idGenerator.js';
5
+ export * from './logger.js';
6
+ export * from './rateLimiter.js';
7
+ export * from './sanitization.js';
8
+ export * from './tokenCounter.js';
9
+ export * from './jsonParser.js';
10
+ // No need for explicit named imports/exports or default export
11
+ // when using wildcard exports for a simple barrel file.
@@ -0,0 +1,78 @@
1
+ import { parse as parsePartialJson, Allow as PartialJsonAllow } from 'partial-json';
2
+ import { BaseErrorCode, McpError } from '../types-global/errors.js';
3
+ import { logger } from './logger.js'; // Import logger
4
+ /**
5
+ * Enum mirroring partial-json's Allow constants for specifying
6
+ * what types of partial JSON structures are permissible during parsing.
7
+ * Use bitwise OR to combine options (e.g., Allow.STR | Allow.OBJ).
8
+ */
9
+ export const Allow = PartialJsonAllow;
10
+ // Regex to find a <think> block at the start, capturing its content and the rest of the string
11
+ const thinkBlockRegex = /^<think>([\s\S]*?)<\/think>\s*([\s\S]*)$/;
12
+ /**
13
+ * Utility class for parsing potentially partial JSON strings.
14
+ * Wraps the 'partial-json' library to provide a consistent interface
15
+ * within the atlas-mcp-agent project.
16
+ * Handles optional <think>...</think> blocks at the beginning of the input.
17
+ */
18
+ class JsonParser {
19
+ /**
20
+ * Parses a JSON string, potentially allowing for incomplete structures
21
+ * and handling optional <think> blocks at the start.
22
+ *
23
+ * @param jsonString The JSON string to parse.
24
+ * @param allowPartial A bitwise OR combination of 'Allow' constants specifying permissible partial types (defaults to Allow.ALL).
25
+ * @param context Optional RequestContext for error correlation and logging think blocks.
26
+ * @returns The parsed JavaScript value.
27
+ * @throws {McpError} Throws an McpError with BaseErrorCode.VALIDATION_ERROR if parsing fails due to malformed JSON.
28
+ */
29
+ parse(jsonString, allowPartial = Allow.ALL, context) {
30
+ let stringToParse = jsonString;
31
+ const match = jsonString.match(thinkBlockRegex);
32
+ if (match) {
33
+ const thinkContent = match[1].trim();
34
+ const restOfString = match[2];
35
+ if (thinkContent) {
36
+ logger.debug('LLM <think> block detected and logged.', { ...context, thinkContent });
37
+ }
38
+ else {
39
+ logger.debug('Empty LLM <think> block detected.', context);
40
+ }
41
+ stringToParse = restOfString; // Parse only the part after </think>
42
+ }
43
+ // Trim leading/trailing whitespace which might interfere with JSON parsing, especially if only JSON is left
44
+ stringToParse = stringToParse.trim();
45
+ if (!stringToParse) {
46
+ // If after removing think block and trimming, the string is empty, it's an error
47
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'JSON string is empty after removing <think> block.', context);
48
+ }
49
+ try {
50
+ // Ensure the string starts with '{' or '[' if we expect an object or array after stripping <think>
51
+ // This helps catch cases where only non-JSON text remains.
52
+ if (!stringToParse.startsWith('{') && !stringToParse.startsWith('[')) {
53
+ // Check if it might be a simple string value that partial-json could parse
54
+ // Allow simple strings only if specifically permitted or Allow.ALL is used
55
+ const allowsString = (allowPartial & Allow.STR) === Allow.STR;
56
+ if (!allowsString && !stringToParse.startsWith('"')) { // Allow quoted strings if Allow.STR is set
57
+ throw new Error('Remaining content does not appear to be valid JSON object or array.');
58
+ }
59
+ // If it starts with a quote and strings are allowed, let parsePartialJson handle it
60
+ }
61
+ return parsePartialJson(stringToParse, allowPartial);
62
+ }
63
+ catch (error) {
64
+ // Wrap the original error in an McpError for consistent error handling
65
+ // Include the original error message for better debugging context.
66
+ logger.error('Failed to parse JSON content.', { ...context, error: error.message, contentAttempted: stringToParse });
67
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to parse JSON: ${error.message}`, {
68
+ ...context,
69
+ originalContent: stringToParse,
70
+ rawError: error instanceof Error ? error.stack : String(error) // Include raw error info
71
+ });
72
+ }
73
+ }
74
+ }
75
+ /**
76
+ * Singleton instance of the JsonParser utility.
77
+ */
78
+ export const jsonParser = new JsonParser();