mcp-code-context 3.0.6 → 3.4.0

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 (75) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/CHANGELOG_v3.2.0.md +196 -0
  3. package/CHANGELOG_v3.3.0.md +266 -0
  4. package/README.md +1 -1
  5. package/SECURITY.md +177 -0
  6. package/dist/src/core/engine.d.ts +2 -2
  7. package/dist/src/core/engine.js +14 -8
  8. package/dist/src/core/engine.js.map +1 -1
  9. package/dist/src/core/validator.d.ts +2 -2
  10. package/dist/src/core/validator.js +8 -7
  11. package/dist/src/core/validator.js.map +1 -1
  12. package/dist/src/index.d.ts +6 -6
  13. package/dist/src/index.js +499 -158
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/operations/compress.d.ts +18 -0
  16. package/dist/src/operations/compress.js +89 -0
  17. package/dist/src/operations/compress.js.map +1 -0
  18. package/dist/src/operations/confirmationStore.d.ts +53 -0
  19. package/dist/src/operations/confirmationStore.js +91 -0
  20. package/dist/src/operations/confirmationStore.js.map +1 -0
  21. package/dist/src/operations/read.d.ts +50 -0
  22. package/dist/src/operations/read.js +228 -0
  23. package/dist/src/operations/read.js.map +1 -0
  24. package/dist/src/operations/write.d.ts +16 -4
  25. package/dist/src/operations/write.js +142 -48
  26. package/dist/src/operations/write.js.map +1 -1
  27. package/dist/src/parsers/base.d.ts +7 -2
  28. package/dist/src/parsers/base.js +18 -1
  29. package/dist/src/parsers/base.js.map +1 -1
  30. package/dist/src/parsers/dartTreeSitter.d.ts +3 -3
  31. package/dist/src/parsers/dartTreeSitter.js +6 -11
  32. package/dist/src/parsers/dartTreeSitter.js.map +1 -1
  33. package/dist/src/parsers/php.d.ts +2 -2
  34. package/dist/src/parsers/php.js +5 -10
  35. package/dist/src/parsers/php.js.map +1 -1
  36. package/dist/src/parsers/python.d.ts +2 -2
  37. package/dist/src/parsers/python.js +5 -10
  38. package/dist/src/parsers/python.js.map +1 -1
  39. package/dist/src/parsers/registry.d.ts +1 -1
  40. package/dist/src/parsers/registry.js +2 -2
  41. package/dist/src/parsers/registry.js.map +1 -1
  42. package/dist/src/parsers/typescript.d.ts +2 -2
  43. package/dist/src/parsers/typescript.js +8 -13
  44. package/dist/src/parsers/typescript.js.map +1 -1
  45. package/dist/src/utils/auditLogger.d.ts +90 -0
  46. package/dist/src/utils/auditLogger.js +271 -0
  47. package/dist/src/utils/auditLogger.js.map +1 -0
  48. package/dist/src/utils/backupManager.d.ts +33 -0
  49. package/dist/src/utils/backupManager.js +130 -0
  50. package/dist/src/utils/backupManager.js.map +1 -0
  51. package/dist/src/utils/constants.d.ts +12 -0
  52. package/dist/src/utils/constants.js +33 -0
  53. package/dist/src/utils/constants.js.map +1 -0
  54. package/dist/src/utils/diff.d.ts +13 -0
  55. package/dist/src/utils/diff.js +124 -0
  56. package/dist/src/utils/diff.js.map +1 -0
  57. package/dist/src/utils/fileLock.d.ts +63 -0
  58. package/dist/src/utils/fileLock.js +123 -0
  59. package/dist/src/utils/fileLock.js.map +1 -0
  60. package/dist/src/utils/rateLimiter.d.ts +61 -0
  61. package/dist/src/utils/rateLimiter.js +114 -0
  62. package/dist/src/utils/rateLimiter.js.map +1 -0
  63. package/dist/src/utils/safeRegex.d.ts +41 -0
  64. package/dist/src/utils/safeRegex.js +137 -0
  65. package/dist/src/utils/safeRegex.js.map +1 -0
  66. package/dist/src/utils/streaming.d.ts +43 -0
  67. package/dist/src/utils/streaming.js +175 -0
  68. package/dist/src/utils/streaming.js.map +1 -0
  69. package/dist/src/utils/telemetry.d.ts +91 -0
  70. package/dist/src/utils/telemetry.js +185 -0
  71. package/dist/src/utils/telemetry.js.map +1 -0
  72. package/llms.txt +46 -28
  73. package/package.json +5 -5
  74. package/mcp-code-context-3.0.2.tgz +0 -0
  75. package/tsconfig.tests.json +0 -27
package/dist/src/index.js CHANGED
@@ -1,28 +1,113 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * mcp-code-context v3.0.6 - Tree-sitter WASM Edition
3
+ * mcp-code-context v3.4.0 - Tree-sitter WASM Edition
4
4
  *
5
- * Complete rewrite with:
5
+ * Production-ready with:
6
6
  * - Tree-sitter WASM for 100% AST accuracy (TypeScript, Python, PHP, Dart)
7
- * - Zero native dependencies (no Visual Studio, node-gyp, or Python required)
8
- * - 100% portable across Windows/Mac/Linux
9
- * - Async-first architecture
10
- * - Mandatory security boundaries
7
+ * - Zero native dependencies — 100% portable across Windows/Mac/Linux
8
+ * - Two-phase write workflow (dry-run + confirmation token)
9
+ * - Middleware pipeline: rate limiting, file locking, audit logging, telemetry
10
+ * - Mandatory security boundaries on ALL handlers
11
11
  */
12
12
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
14
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
15
+ import * as path from "node:path";
15
16
  import { CodeContextEngine } from "./core/engine.js";
16
17
  import { ParserRegistry } from "./parsers/registry.js";
17
18
  import { SecurityValidator } from "./core/validator.js";
18
- import { replaceSymbol, insertCode, removeSymbol, writeFile } from "./operations/write.js";
19
+ import { replaceSymbol, insertCode, removeSymbol, writeFile, renameSymbol } from "./operations/write.js";
20
+ import { extractSymbol, readLines, searchPattern, analyzeImpact } from "./operations/read.js";
21
+ import { compressRepository } from "./operations/compress.js";
22
+ import { globalConfirmationStore } from "./operations/confirmationStore.js";
23
+ import { globalAuditLogger } from "./utils/auditLogger.js";
24
+ import { globalTelemetry } from "./utils/telemetry.js";
25
+ import { RateLimiter, OPERATION_COSTS } from "./utils/rateLimiter.js";
26
+ import { globalLockManager } from "./utils/fileLock.js";
27
+ import { streamFile } from "./utils/streaming.js";
28
+ import { BackupManager } from "./utils/backupManager.js";
29
+ import * as fs from "node:fs/promises";
19
30
  const SERVER_NAME = "mcp-code-context";
20
- const SERVER_VERSION = "3.0.6";
31
+ const SERVER_VERSION = "3.4.0";
21
32
  // Global instances
22
33
  let engine;
23
34
  let registry;
24
- const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
35
+ const rateLimiter = new RateLimiter();
36
+ // Write operations that require file locking and two-phase workflow
37
+ const WRITE_OPS = new Set(["replace_symbol", "insert_symbol", "remove_symbol", "rename_symbol"]);
25
38
  const TOOLS = [
39
+ {
40
+ name: "get_semantic_repo_map",
41
+ description: "Generate a compressed architectural overview of an entire repository",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ directoryPath: { type: "string", description: "Absolute path to repository root" },
46
+ projectRoot: { type: "string", description: "Project root for security boundary" },
47
+ format: { type: "string", enum: ["xml", "markdown"], description: "Output format (default: xml)" },
48
+ },
49
+ required: ["directoryPath", "projectRoot"],
50
+ },
51
+ },
52
+ {
53
+ name: "read_file_surgical",
54
+ description: "Read a file or extract a specific named symbol",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ filePath: { type: "string", description: "Absolute path to file" },
59
+ projectRoot: { type: "string", description: "Project root (REQUIRED)" },
60
+ symbolName: { type: "string", description: "Symbol name to extract (optional)" },
61
+ className: { type: "string", description: "Class name for scoping (optional)" },
62
+ },
63
+ required: ["filePath", "projectRoot"],
64
+ },
65
+ },
66
+ {
67
+ name: "analyze_impact",
68
+ description: "Find all files that depend on a given file",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ filePath: { type: "string", description: "Absolute path to file" },
73
+ projectRoot: { type: "string", description: "Project root for security boundary" },
74
+ rootDir: { type: "string", description: "Repository root (optional, defaults to projectRoot)" },
75
+ },
76
+ required: ["filePath", "projectRoot"],
77
+ },
78
+ },
79
+ {
80
+ name: "read_file_lines",
81
+ description: "Read specific line ranges from a file",
82
+ inputSchema: {
83
+ type: "object",
84
+ properties: {
85
+ filePath: { type: "string", description: "Absolute path to file" },
86
+ projectRoot: { type: "string", description: "Project root for security boundary" },
87
+ startLine: { type: "number", description: "Starting line number (1-indexed)" },
88
+ endLine: { type: "number", description: "Ending line number (1-indexed)" },
89
+ aroundPattern: { type: "string", description: "Search pattern to find and return surrounding lines" },
90
+ contextLines: { type: "number", description: "Number of lines before/after pattern (default: 5)" },
91
+ },
92
+ required: ["filePath", "projectRoot"],
93
+ },
94
+ },
95
+ {
96
+ name: "search_code_pattern",
97
+ description: "Search for code patterns across multiple files",
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ rootDir: { type: "string", description: "Repository root directory" },
102
+ projectRoot: { type: "string", description: "Project root for security boundary" },
103
+ pattern: { type: "string", description: "Regular expression pattern to search" },
104
+ fileExtensions: { type: "array", items: { type: "string" }, description: "Extensions to search" },
105
+ excludeDirs: { type: "array", items: { type: "string" }, description: "Directories to exclude" },
106
+ maxResults: { type: "number", description: "Maximum matches to return (default: 50)" },
107
+ },
108
+ required: ["rootDir", "projectRoot", "pattern"],
109
+ },
110
+ },
26
111
  {
27
112
  name: "parse_file",
28
113
  description: "Parse a file using Tree-sitter and extract symbols",
@@ -36,8 +121,8 @@ const TOOLS = [
36
121
  },
37
122
  },
38
123
  {
39
- name: "replace_symbol",
40
- description: "Replace a symbol (function/class/method) with new code",
124
+ name: "write_file_surgical",
125
+ description: "Replace a symbol with new code. Phase 1 (dry-run): returns diff + token. Phase 2: confirm with token to apply.",
41
126
  inputSchema: {
42
127
  type: "object",
43
128
  properties: {
@@ -46,13 +131,15 @@ const TOOLS = [
46
131
  symbolName: { type: "string", description: "Symbol to replace" },
47
132
  newContent: { type: "string", description: "New code" },
48
133
  className: { type: "string", description: "Class name (optional, for scoping)" },
134
+ confirm: { type: "boolean", description: "Set true to apply a pending operation" },
135
+ confirmationToken: { type: "string", description: "Token from Phase 1 dry-run" },
49
136
  },
50
137
  required: ["filePath", "projectRoot", "symbolName", "newContent"],
51
138
  },
52
139
  },
53
140
  {
54
- name: "insert_code",
55
- description: "Insert code at a specific location",
141
+ name: "insert_symbol",
142
+ description: "Insert code at a specific location. Phase 1: returns diff + token. Phase 2: confirm with token to apply.",
56
143
  inputSchema: {
57
144
  type: "object",
58
145
  properties: {
@@ -62,13 +149,15 @@ const TOOLS = [
62
149
  anchorSymbol: { type: "string", description: "Symbol to position relative to" },
63
150
  position: { type: "string", enum: ["before", "after", "inside_start", "inside_end"], description: "Where to insert" },
64
151
  className: { type: "string", description: "Class name (optional)" },
152
+ confirm: { type: "boolean", description: "Set true to apply a pending operation" },
153
+ confirmationToken: { type: "string", description: "Token from Phase 1 dry-run" },
65
154
  },
66
155
  required: ["filePath", "projectRoot", "code"],
67
156
  },
68
157
  },
69
158
  {
70
159
  name: "remove_symbol",
71
- description: "Remove a symbol from file",
160
+ description: "Remove a symbol from file. Phase 1: returns diff + token. Phase 2: confirm with token to apply.",
72
161
  inputSchema: {
73
162
  type: "object",
74
163
  properties: {
@@ -76,181 +165,433 @@ const TOOLS = [
76
165
  projectRoot: { type: "string", description: "Project root (REQUIRED)" },
77
166
  symbolName: { type: "string", description: "Symbol to remove" },
78
167
  className: { type: "string", description: "Class name (optional)" },
168
+ force: { type: "boolean", description: "Skip dependency check" },
169
+ confirm: { type: "boolean", description: "Set true to apply a pending operation" },
170
+ confirmationToken: { type: "string", description: "Token from Phase 1 dry-run" },
79
171
  },
80
172
  required: ["filePath", "projectRoot", "symbolName"],
81
173
  },
82
174
  },
175
+ {
176
+ name: "rename_symbol",
177
+ description: "Rename a symbol across the entire repository. Phase 1: returns diff + token. Phase 2: confirm with token to apply.",
178
+ inputSchema: {
179
+ type: "object",
180
+ properties: {
181
+ filePath: { type: "string", description: "File where symbol is defined" },
182
+ projectRoot: { type: "string", description: "Project root (REQUIRED)" },
183
+ oldName: { type: "string", description: "Current name" },
184
+ newName: { type: "string", description: "New name" },
185
+ rootDir: { type: "string", description: "Repository root (optional)" },
186
+ confirm: { type: "boolean", description: "Set true to apply a pending operation" },
187
+ confirmationToken: { type: "string", description: "Token from Phase 1 dry-run" },
188
+ },
189
+ required: ["filePath", "projectRoot", "oldName", "newName"],
190
+ },
191
+ },
192
+ {
193
+ name: "rollback_file",
194
+ description: "Revert a file to its backup state",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ filePath: { type: "string", description: "Absolute path to file" },
199
+ projectRoot: { type: "string", description: "Project root (REQUIRED)" },
200
+ },
201
+ required: ["filePath", "projectRoot"],
202
+ },
203
+ },
204
+ {
205
+ name: "clean_backups",
206
+ description: "Remove all backup files for a project",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ projectRoot: { type: "string", description: "Project root directory" },
211
+ },
212
+ required: ["projectRoot"],
213
+ },
214
+ },
215
+ {
216
+ name: "get_server_stats",
217
+ description: "Get server telemetry, audit statistics, and health metrics",
218
+ inputSchema: {
219
+ type: "object",
220
+ properties: {},
221
+ },
222
+ },
83
223
  ];
224
+ const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
84
225
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
85
226
  tools: TOOLS,
86
227
  }));
87
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
88
- const { name, arguments: args } = request.params;
89
- try {
90
- switch (name) {
91
- case "parse_file":
92
- return await handleParseFile(args);
93
- case "replace_symbol":
94
- return await handleReplaceSymbol(args);
95
- case "insert_code":
96
- return await handleInsertCode(args);
97
- case "remove_symbol":
98
- return await handleRemoveSymbol(args);
99
- default:
100
- return errorResponse(`Unknown tool: "${name}"`);
101
- }
228
+ // Setup connection and initialize engine
229
+ async function main() {
230
+ engine = new CodeContextEngine();
231
+ await engine.init();
232
+ registry = new ParserRegistry(engine);
233
+ await registry.init();
234
+ const transport = new StdioServerTransport();
235
+ await server.connect(transport);
236
+ console.error(`[${SERVER_NAME}] v${SERVER_VERSION} running on stdio`);
237
+ }
238
+ // -----------------------------------------------------------------------------
239
+ // READ HANDLERS
240
+ // -----------------------------------------------------------------------------
241
+ async function handleGetSemanticRepoMap(args) {
242
+ const directoryPath = String(args.directoryPath);
243
+ const projectRoot = String(args.projectRoot);
244
+ const format = args.format === "markdown" ? "markdown" : "xml";
245
+ const validator = new SecurityValidator(projectRoot);
246
+ const validation = await validator.validateFilePath(directoryPath);
247
+ if (!validation.valid) {
248
+ throw new Error(validation.error);
102
249
  }
103
- catch (error) {
104
- const message = error instanceof Error ? error.message : String(error);
105
- return errorResponse(`Error in tool "${name}": ${message}`);
250
+ const result = await compressRepository({
251
+ directoryPath: validation.resolvedPath,
252
+ registry,
253
+ format: format
254
+ });
255
+ return {
256
+ content: [{ type: "text", text: result }],
257
+ };
258
+ }
259
+ async function handleReadFileSurgical(args) {
260
+ const filePath = String(args.filePath);
261
+ const projectRoot = String(args.projectRoot);
262
+ const symbolName = args.symbolName ? String(args.symbolName) : undefined;
263
+ const className = args.className ? String(args.className) : undefined;
264
+ const validator = new SecurityValidator(projectRoot);
265
+ const validation = await validator.validateFilePath(filePath);
266
+ if (!validation.valid)
267
+ throw new Error(validation.error);
268
+ let content;
269
+ if (symbolName) {
270
+ const ext = path.extname(filePath);
271
+ const parser = registry.getParser(ext);
272
+ if (!parser)
273
+ throw new Error(`No parser available for ${ext} files`);
274
+ const result = await extractSymbol({ filePath: validation.resolvedPath, projectRoot, symbolName, className, parser });
275
+ if (!result.success)
276
+ throw new Error(result.error);
277
+ content = result.content;
106
278
  }
107
- });
279
+ else {
280
+ // If > 5MB, stream it to avoid memory issues
281
+ const stat = await fs.stat(validation.resolvedPath);
282
+ if (stat.size > 5 * 1024 * 1024) {
283
+ const streamRes = await streamFile(validation.resolvedPath);
284
+ if (!streamRes.success)
285
+ throw new Error(streamRes.error);
286
+ content = streamRes.chunks.join("");
287
+ }
288
+ else {
289
+ content = await fs.readFile(validation.resolvedPath, "utf-8");
290
+ }
291
+ }
292
+ return { content: [{ type: "text", text: content }] };
293
+ }
294
+ async function handleAnalyzeImpact(args) {
295
+ const filePath = String(args.filePath);
296
+ const projectRoot = String(args.projectRoot);
297
+ const rootDir = args.rootDir ? String(args.rootDir) : projectRoot;
298
+ const validator = new SecurityValidator(projectRoot);
299
+ const validation = await validator.validateFilePath(filePath);
300
+ if (!validation.valid)
301
+ throw new Error(validation.error);
302
+ const rootValidation = await validator.validateFilePath(rootDir);
303
+ if (!rootValidation.valid)
304
+ throw new Error(rootValidation.error);
305
+ const result = await analyzeImpact({ filePath: validation.resolvedPath, rootDir: rootValidation.resolvedPath });
306
+ if (!result.success)
307
+ throw new Error(result.error);
308
+ return { content: [{ type: "text", text: result.content }] };
309
+ }
310
+ async function handleReadFileLines(args) {
311
+ const filePath = String(args.filePath);
312
+ const projectRoot = String(args.projectRoot);
313
+ const validator = new SecurityValidator(projectRoot);
314
+ const validation = await validator.validateFilePath(filePath);
315
+ if (!validation.valid)
316
+ throw new Error(validation.error);
317
+ const result = await readLines({
318
+ filePath: validation.resolvedPath,
319
+ startLine: args.startLine,
320
+ endLine: args.endLine,
321
+ aroundPattern: args.aroundPattern,
322
+ contextLines: args.contextLines,
323
+ });
324
+ if (!result.success)
325
+ throw new Error(result.error);
326
+ return { content: [{ type: "text", text: result.content }] };
327
+ }
328
+ async function handleSearchCodePattern(args) {
329
+ const rootDir = String(args.rootDir);
330
+ const projectRoot = String(args.projectRoot);
331
+ const validator = new SecurityValidator(projectRoot);
332
+ const validation = await validator.validateFilePath(rootDir);
333
+ if (!validation.valid)
334
+ throw new Error(validation.error);
335
+ const result = await searchPattern({
336
+ rootDir: validation.resolvedPath,
337
+ pattern: String(args.pattern),
338
+ fileExtensions: args.fileExtensions,
339
+ excludeDirs: args.excludeDirs,
340
+ maxResults: args.maxResults,
341
+ });
342
+ if (!result.success)
343
+ throw new Error(result.error);
344
+ return { content: [{ type: "text", text: result.content }] };
345
+ }
108
346
  async function handleParseFile(args) {
109
- const filePath = args.filePath;
110
- const projectRoot = args.projectRoot;
111
- if (!filePath)
112
- return errorResponse("Missing required parameter: filePath");
113
- if (!projectRoot)
114
- return errorResponse("Missing required parameter: projectRoot");
347
+ const filePath = String(args.filePath);
348
+ const projectRoot = String(args.projectRoot);
115
349
  const validator = new SecurityValidator(projectRoot);
116
350
  const validation = await validator.validateFilePath(filePath);
117
- if (!validation.valid) {
118
- return errorResponse(validation.error);
119
- }
120
- const fs = await import("node:fs/promises");
351
+ if (!validation.valid)
352
+ throw new Error(validation.error);
121
353
  const content = await fs.readFile(validation.resolvedPath, "utf-8");
122
- const path = await import("node:path");
123
354
  const ext = path.extname(validation.resolvedPath);
124
355
  const parser = registry.getParser(ext);
125
- if (!parser) {
126
- return errorResponse(`No parser available for extension: ${ext}`);
127
- }
356
+ if (!parser)
357
+ throw new Error(`No parser available for ${ext} files`);
128
358
  const tree = parser.parse(content);
129
359
  const symbols = parser.findSymbols(tree);
130
360
  return {
131
- content: [
132
- {
133
- type: "text",
134
- text: JSON.stringify({ file: filePath, symbols }, null, 2),
135
- },
136
- ],
361
+ content: [{ type: "text", text: JSON.stringify(symbols, null, 2) }],
137
362
  };
138
363
  }
139
- async function handleReplaceSymbol(args) {
140
- const filePath = args.filePath;
141
- const projectRoot = args.projectRoot;
142
- const symbolName = args.symbolName;
143
- const newContent = args.newContent;
144
- const className = args.className;
145
- if (!filePath)
146
- return errorResponse("Missing: filePath");
147
- if (!projectRoot)
148
- return errorResponse("Missing: projectRoot");
149
- if (!symbolName)
150
- return errorResponse("Missing: symbolName");
151
- if (!newContent)
152
- return errorResponse("Missing: newContent");
153
- const path = await import("node:path");
154
- const ext = path.extname(filePath);
155
- const parser = registry.getParser(ext);
156
- if (!parser)
157
- return errorResponse(`No parser for: ${ext}`);
158
- const result = await replaceSymbol({ filePath, projectRoot, symbolName, newContent, className, parser });
159
- if (!result.success) {
160
- return errorResponse(result.error);
161
- }
162
- // Write to file
163
- const validator = new SecurityValidator(projectRoot);
164
- const validation = await validator.validateFilePath(filePath);
165
- if (validation.valid) {
166
- await writeFile(validation.resolvedPath, result.newContent);
364
+ // -----------------------------------------------------------------------------
365
+ // WRITE HANDLERS (Two-Phase)
366
+ // -----------------------------------------------------------------------------
367
+ async function handleTwoPhaseWrite(args, operationName, executeWrite) {
368
+ const confirm = Boolean(args.confirm);
369
+ const token = args.confirmationToken ? String(args.confirmationToken) : undefined;
370
+ if (confirm) {
371
+ if (!token)
372
+ throw new Error("confirmationToken is required when confirm=true");
373
+ const pendingOp = globalConfirmationStore.consumePending(token);
374
+ if (!pendingOp)
375
+ throw new Error(`Invalid or expired confirmation token: ${token}`);
376
+ // Create backup before applying
377
+ await BackupManager.createBackup(pendingOp.filePath, String(args.projectRoot));
378
+ // Apply the write
379
+ await writeFile(pendingOp.filePath, pendingOp.newContent);
380
+ return {
381
+ content: [{ type: "text", text: `Success. Changes applied to ${pendingOp.filePath}\n\nDiff:\n${pendingOp.diff}` }],
382
+ };
167
383
  }
384
+ // Phase 1: Dry-run
385
+ const result = await executeWrite();
386
+ if (!result.success)
387
+ throw new Error(result.error);
388
+ const newToken = globalConfirmationStore.storePending({
389
+ filePath: String(args.filePath),
390
+ operation: operationName,
391
+ symbolName: args.symbolName ? String(args.symbolName) : undefined,
392
+ newContent: result.newContent,
393
+ diff: result.diff,
394
+ });
168
395
  return {
169
- content: [{ type: "text", text: `✅ Replaced "${symbolName}"\n\n${result.diff}` }],
396
+ content: [{
397
+ type: "text",
398
+ text: `DRY RUN SUCCESSFUL. Please review the diff below.\nTo apply these changes, call this tool again with confirm=true and confirmationToken="${newToken}"\n\nDiff:\n${result.diff}`
399
+ }],
170
400
  };
171
401
  }
172
- async function handleInsertCode(args) {
173
- const filePath = args.filePath;
174
- const projectRoot = args.projectRoot;
175
- const code = args.code;
176
- const anchorSymbol = args.anchorSymbol;
177
- const position = args.position;
178
- const className = args.className;
179
- if (!filePath)
180
- return errorResponse("Missing: filePath");
181
- if (!projectRoot)
182
- return errorResponse("Missing: projectRoot");
183
- if (!code)
184
- return errorResponse("Missing: code");
185
- const path = await import("node:path");
186
- const ext = path.extname(filePath);
187
- const parser = registry.getParser(ext);
188
- if (!parser)
189
- return errorResponse(`No parser for: ${ext}`);
190
- const result = await insertCode({ filePath, projectRoot, code, anchorSymbol, position, className, parser });
191
- if (!result.success) {
192
- return errorResponse(result.error);
193
- }
194
- const validator = new SecurityValidator(projectRoot);
195
- const validation = await validator.validateFilePath(filePath);
196
- if (validation.valid) {
197
- await writeFile(validation.resolvedPath, result.newContent);
198
- }
199
- return {
200
- content: [{ type: "text", text: `✅ Code inserted\n\n${result.diff}` }],
201
- };
402
+ async function handleWriteFileSurgical(args) {
403
+ return handleTwoPhaseWrite(args, "write_file_surgical", async () => {
404
+ const ext = path.extname(String(args.filePath));
405
+ const parser = registry.getParser(ext);
406
+ if (!parser)
407
+ throw new Error(`No parser available for ${ext} files`);
408
+ return await replaceSymbol({
409
+ filePath: String(args.filePath),
410
+ projectRoot: String(args.projectRoot),
411
+ symbolName: String(args.symbolName),
412
+ newContent: String(args.newContent),
413
+ className: args.className ? String(args.className) : undefined,
414
+ parser,
415
+ });
416
+ });
417
+ }
418
+ async function handleInsertSymbol(args) {
419
+ return handleTwoPhaseWrite(args, "insert_symbol", async () => {
420
+ const ext = path.extname(String(args.filePath));
421
+ const parser = registry.getParser(ext);
422
+ if (!parser)
423
+ throw new Error(`No parser available for ${ext} files`);
424
+ return await insertCode({
425
+ filePath: String(args.filePath),
426
+ projectRoot: String(args.projectRoot),
427
+ code: String(args.code),
428
+ anchorSymbol: args.anchorSymbol ? String(args.anchorSymbol) : undefined,
429
+ position: args.position,
430
+ className: args.className ? String(args.className) : undefined,
431
+ parser,
432
+ });
433
+ });
202
434
  }
203
435
  async function handleRemoveSymbol(args) {
204
- const filePath = args.filePath;
205
- const projectRoot = args.projectRoot;
206
- const symbolName = args.symbolName;
207
- const className = args.className;
208
- if (!filePath)
209
- return errorResponse("Missing: filePath");
210
- if (!projectRoot)
211
- return errorResponse("Missing: projectRoot");
212
- if (!symbolName)
213
- return errorResponse("Missing: symbolName");
214
- const path = await import("node:path");
215
- const ext = path.extname(filePath);
216
- const parser = registry.getParser(ext);
217
- if (!parser)
218
- return errorResponse(`No parser for: ${ext}`);
219
- const result = await removeSymbol({ filePath, projectRoot, symbolName, className, parser });
220
- if (!result.success) {
221
- return errorResponse(result.error);
222
- }
436
+ return handleTwoPhaseWrite(args, "remove_symbol", async () => {
437
+ const ext = path.extname(String(args.filePath));
438
+ const parser = registry.getParser(ext);
439
+ if (!parser)
440
+ throw new Error(`No parser available for ${ext} files`);
441
+ return await removeSymbol({
442
+ filePath: String(args.filePath),
443
+ projectRoot: String(args.projectRoot),
444
+ symbolName: String(args.symbolName),
445
+ className: args.className ? String(args.className) : undefined,
446
+ parser,
447
+ });
448
+ });
449
+ }
450
+ async function handleRenameSymbol(args) {
451
+ return handleTwoPhaseWrite(args, "rename_symbol", async () => {
452
+ const ext = path.extname(String(args.filePath));
453
+ const parser = registry.getParser(ext);
454
+ if (!parser)
455
+ throw new Error(`No parser available for ${ext} files`);
456
+ return await renameSymbol({
457
+ filePath: String(args.filePath),
458
+ projectRoot: String(args.projectRoot),
459
+ oldName: String(args.oldName),
460
+ newName: String(args.newName),
461
+ rootDir: args.rootDir ? String(args.rootDir) : String(args.projectRoot),
462
+ parser,
463
+ });
464
+ });
465
+ }
466
+ // -----------------------------------------------------------------------------
467
+ // OTHER HANDLERS
468
+ // -----------------------------------------------------------------------------
469
+ async function handleRollbackFile(args) {
470
+ const filePath = String(args.filePath);
471
+ const projectRoot = String(args.projectRoot);
472
+ const steps = args.steps ? Number(args.steps) : 1;
223
473
  const validator = new SecurityValidator(projectRoot);
224
474
  const validation = await validator.validateFilePath(filePath);
225
- if (validation.valid) {
226
- await writeFile(validation.resolvedPath, result.newContent);
227
- }
228
- return {
229
- content: [{ type: "text", text: `✅ Removed "${symbolName}"\n\n${result.diff}` }],
230
- };
475
+ if (!validation.valid)
476
+ throw new Error(validation.error);
477
+ const result = await BackupManager.rollback(validation.resolvedPath, projectRoot, steps);
478
+ if (!result.success)
479
+ throw new Error(result.error);
480
+ return { content: [{ type: "text", text: `Successfully rolled back ${path.basename(filePath)} from backup: ${result.restoredFrom}` }] };
231
481
  }
232
- function errorResponse(message) {
482
+ async function handleCleanBackups(args) {
483
+ const projectRoot = String(args.projectRoot);
484
+ const validator = new SecurityValidator(projectRoot);
485
+ // Just validate projectRoot is a valid directory
486
+ const validation = await validator.validateFilePath(projectRoot);
487
+ if (!validation.valid)
488
+ throw new Error(validation.error);
489
+ const result = await BackupManager.clean(projectRoot);
490
+ if (!result.success)
491
+ throw new Error(result.error);
492
+ return { content: [{ type: "text", text: `Successfully cleaned backups. Deleted ${result.deletedCount} backup files.` }] };
493
+ }
494
+ async function handleGetServerStats() {
495
+ const stats = {
496
+ pendingConfirmations: globalConfirmationStore.getPendingCount(),
497
+ // Telemetry and audit stats can be exposed here later
498
+ };
233
499
  return {
234
- content: [{ type: "text", text: `❌ ${message}` }],
235
- isError: true,
500
+ content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
236
501
  };
237
502
  }
238
- async function main() {
239
- console.error("🚀 Initializing mcp-code-context v3.0.6 (WASM)...");
240
- engine = new CodeContextEngine();
241
- await engine.init();
242
- registry = new ParserRegistry(engine);
243
- await registry.init();
244
- console.error("✅ Engine initialized with Tree-sitter WASM (portable)");
245
- const transport = new StdioServerTransport();
246
- await server.connect(transport);
247
- console.error(`🚀 ${SERVER_NAME} v${SERVER_VERSION} running on stdio`);
248
- }
249
- import { pathToFileURL } from "node:url";
250
- if (import.meta.url === pathToFileURL(process.argv[1]).href) {
251
- main().catch((error) => {
252
- console.error("Fatal error starting MCP server:", error);
253
- process.exit(1);
254
- });
255
- }
503
+ // -----------------------------------------------------------------------------
504
+ // PIPELINE & MAIN EXECUTION
505
+ // -----------------------------------------------------------------------------
506
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
507
+ const { name, arguments: args } = request.params;
508
+ const startTime = Date.now();
509
+ let success = false;
510
+ let lockReleased = false;
511
+ try {
512
+ // 1. Rate Limiting Middleware
513
+ const cost = OPERATION_COSTS[name] ?? 1;
514
+ const rateCheck = await rateLimiter.checkLimit(name, cost);
515
+ if (!rateCheck.allowed) {
516
+ throw new Error(`Rate limit exceeded for tool: ${name}. Retry after ${rateCheck.retryAfter}ms`);
517
+ }
518
+ // 2. File Locking Middleware (for writes only)
519
+ if (WRITE_OPS.has(name) && args && args.filePath) {
520
+ await globalLockManager.acquireLock(String(args.filePath), "mcp-client", name);
521
+ }
522
+ // 3. Execution
523
+ let result;
524
+ switch (name) {
525
+ case "get_semantic_repo_map":
526
+ result = await handleGetSemanticRepoMap(args);
527
+ break;
528
+ case "read_file_surgical":
529
+ result = await handleReadFileSurgical(args);
530
+ break;
531
+ case "analyze_impact":
532
+ result = await handleAnalyzeImpact(args);
533
+ break;
534
+ case "read_file_lines":
535
+ result = await handleReadFileLines(args);
536
+ break;
537
+ case "search_code_pattern":
538
+ result = await handleSearchCodePattern(args);
539
+ break;
540
+ case "parse_file":
541
+ result = await handleParseFile(args);
542
+ break;
543
+ case "write_file_surgical":
544
+ result = await handleWriteFileSurgical(args);
545
+ break;
546
+ case "insert_symbol":
547
+ result = await handleInsertSymbol(args);
548
+ break;
549
+ case "remove_symbol":
550
+ result = await handleRemoveSymbol(args);
551
+ break;
552
+ case "rename_symbol":
553
+ result = await handleRenameSymbol(args);
554
+ break;
555
+ case "rollback_file":
556
+ result = await handleRollbackFile(args);
557
+ break;
558
+ case "clean_backups":
559
+ result = await handleCleanBackups(args);
560
+ break;
561
+ case "get_server_stats":
562
+ result = await handleGetServerStats();
563
+ break;
564
+ default:
565
+ throw new Error(`Unknown tool: ${name}`);
566
+ }
567
+ success = true;
568
+ return result;
569
+ }
570
+ catch (error) {
571
+ return {
572
+ content: [{ type: "text", text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` }],
573
+ isError: true,
574
+ };
575
+ }
576
+ finally {
577
+ const duration = Date.now() - startTime;
578
+ // 4. File Lock Release
579
+ if (WRITE_OPS.has(name) && args && args.filePath && !lockReleased) {
580
+ globalLockManager.releaseLock(String(args.filePath), "mcp-client");
581
+ lockReleased = true;
582
+ }
583
+ // 5. Telemetry & Audit Logging Middleware
584
+ globalTelemetry.recordOperation({ toolName: name, duration, success });
585
+ globalAuditLogger.log({
586
+ level: success ? "info" : "error",
587
+ operation: name,
588
+ details: args ? args : {},
589
+ result: success ? "success" : "failure"
590
+ });
591
+ }
592
+ });
593
+ main().catch((error) => {
594
+ console.error("Server error:", error);
595
+ process.exit(1);
596
+ });
256
597
  //# sourceMappingURL=index.js.map