mcp-code-context 2.2.0 → 2.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 (132) hide show
  1. package/.io/RULES.md +12 -0
  2. package/CHANGELOG.md +114 -1
  3. package/README.md +28 -1
  4. package/dist/src/ast/writers/safeRename.d.ts +14 -0
  5. package/dist/src/ast/writers/safeRename.js +258 -0
  6. package/dist/src/ast/writers/safeRename.js.map +1 -0
  7. package/dist/src/ast/writers/symbolWriter.d.ts +2 -0
  8. package/dist/src/ast/writers/symbolWriter.js +9 -14
  9. package/dist/src/ast/writers/symbolWriter.js.map +1 -1
  10. package/dist/src/cache/astCache.d.ts +33 -0
  11. package/dist/src/cache/astCache.js +113 -0
  12. package/dist/src/cache/astCache.js.map +1 -0
  13. package/dist/src/handlers/readHandlers.d.ts +70 -0
  14. package/dist/src/handlers/readHandlers.js +431 -0
  15. package/dist/src/handlers/readHandlers.js.map +1 -0
  16. package/dist/src/handlers/utilHandlers.d.ts +31 -0
  17. package/dist/src/handlers/utilHandlers.js +69 -0
  18. package/dist/src/handlers/utilHandlers.js.map +1 -0
  19. package/dist/src/handlers/writeHandlers.d.ts +57 -0
  20. package/dist/src/handlers/writeHandlers.js +475 -0
  21. package/dist/src/handlers/writeHandlers.js.map +1 -0
  22. package/dist/src/index.d.ts +6 -143
  23. package/dist/src/index.js +77 -1091
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/tools/searchCodePattern.d.ts +7 -2
  26. package/dist/src/tools/searchCodePattern.js +68 -4
  27. package/dist/src/tools/searchCodePattern.js.map +1 -1
  28. package/dist/src/utils/arrayUtils.d.ts +12 -0
  29. package/dist/src/utils/arrayUtils.js +27 -0
  30. package/dist/src/utils/arrayUtils.js.map +1 -0
  31. package/dist/src/utils/constants.d.ts +46 -0
  32. package/dist/src/utils/constants.js +112 -0
  33. package/dist/src/utils/constants.js.map +1 -0
  34. package/dist/src/utils/ignoreManager.d.ts +8 -0
  35. package/dist/src/utils/ignoreManager.js +49 -0
  36. package/dist/src/utils/ignoreManager.js.map +1 -1
  37. package/dist/src/utils/normalization.d.ts +49 -0
  38. package/dist/src/utils/normalization.js +173 -0
  39. package/dist/src/utils/normalization.js.map +1 -0
  40. package/dist/src/utils/projectRoot.d.ts +12 -5
  41. package/dist/src/utils/projectRoot.js +22 -20
  42. package/dist/src/utils/projectRoot.js.map +1 -1
  43. package/dist/src/utils/secureValidation.d.ts +15 -0
  44. package/dist/src/utils/secureValidation.js +41 -0
  45. package/dist/src/utils/secureValidation.js.map +1 -0
  46. package/dist/src/utils/transactionManager.d.ts +45 -0
  47. package/dist/src/utils/transactionManager.js +102 -0
  48. package/dist/src/utils/transactionManager.js.map +1 -0
  49. package/dist/src/utils/validation.d.ts +87 -0
  50. package/dist/src/utils/validation.js +231 -0
  51. package/dist/src/utils/validation.js.map +1 -0
  52. package/docs/ARCHITECTURE.md +300 -0
  53. package/docs/architecture/ADR-001-modular-handlers.md +119 -0
  54. package/llms.txt +10 -1
  55. package/package.json +5 -5
  56. package/AGENT_INSTRUCTIONS.md +0 -135
  57. package/DEPLOYMENT_INSTRUCTIONS.md +0 -110
  58. package/MEJORAS_PROPUESTAS.md +0 -324
  59. package/RELEASE_NOTES_v2.1.3.md +0 -78
  60. package/RELEASE_NOTES_v2.2.0.md +0 -286
  61. package/dist/src/tests/test-confirmation-flow.d.ts +0 -1
  62. package/dist/src/tests/test-confirmation-flow.js +0 -43
  63. package/dist/src/tests/test-confirmation-flow.js.map +0 -1
  64. package/dist/tests/debug-replace.d.ts +0 -1
  65. package/dist/tests/debug-replace.js +0 -17
  66. package/dist/tests/debug-replace.js.map +0 -1
  67. package/dist/tests/test-backup-system.d.ts +0 -7
  68. package/dist/tests/test-backup-system.js +0 -152
  69. package/dist/tests/test-backup-system.js.map +0 -1
  70. package/dist/tests/test-confirmation-flow.d.ts +0 -1
  71. package/dist/tests/test-confirmation-flow.js +0 -43
  72. package/dist/tests/test-confirmation-flow.js.map +0 -1
  73. package/dist/tests/test-dart.d.ts +0 -1
  74. package/dist/tests/test-dart.js +0 -242
  75. package/dist/tests/test-dart.js.map +0 -1
  76. package/dist/tests/test-new-tools.d.ts +0 -4
  77. package/dist/tests/test-new-tools.js +0 -147
  78. package/dist/tests/test-new-tools.js.map +0 -1
  79. package/dist/tests/test-php-ast.d.ts +0 -1
  80. package/dist/tests/test-php-ast.js +0 -152
  81. package/dist/tests/test-php-ast.js.map +0 -1
  82. package/dist/tests/test-php.d.ts +0 -1
  83. package/dist/tests/test-php.js +0 -234
  84. package/dist/tests/test-php.js.map +0 -1
  85. package/dist/tests/test-smoke.d.ts +0 -5
  86. package/dist/tests/test-smoke.js +0 -83
  87. package/dist/tests/test-smoke.js.map +0 -1
  88. package/dist/tests/writers/test-crlf-handling.d.ts +0 -4
  89. package/dist/tests/writers/test-crlf-handling.js +0 -73
  90. package/dist/tests/writers/test-crlf-handling.js.map +0 -1
  91. package/dist/tests/writers/test-write-smoke.d.ts +0 -9
  92. package/dist/tests/writers/test-write-smoke.js +0 -346
  93. package/dist/tests/writers/test-write-smoke.js.map +0 -1
  94. package/src/ast/dartCompressor.ts +0 -1013
  95. package/src/ast/phpCompressor.ts +0 -814
  96. package/src/ast/semanticCompressor.ts +0 -808
  97. package/src/ast/writers/dartWriter.ts +0 -648
  98. package/src/ast/writers/phpWriter.ts +0 -508
  99. package/src/ast/writers/pyWriter.ts +0 -446
  100. package/src/ast/writers/symbolWriter.ts +0 -277
  101. package/src/ast/writers/tsWriter.ts +0 -418
  102. package/src/index.ts +0 -1784
  103. package/src/tools/readFileLines.ts +0 -104
  104. package/src/tools/searchCodePattern.ts +0 -129
  105. package/src/utils/backupManager.ts +0 -141
  106. package/src/utils/confirmationCache.ts +0 -61
  107. package/src/utils/diffEngine.ts +0 -223
  108. package/src/utils/fuzzyMatch.ts +0 -86
  109. package/src/utils/ignoreManager.ts +0 -144
  110. package/src/utils/projectRoot.ts +0 -44
  111. package/tests/test-backup-system.ts +0 -174
  112. package/tests/test-confirmation-flow.ts +0 -49
  113. package/tests/test-dart.d.ts +0 -1
  114. package/tests/test-dart.js +0 -209
  115. package/tests/test-dart.js.map +0 -1
  116. package/tests/test-dart.ts +0 -265
  117. package/tests/test-new-tools.ts +0 -165
  118. package/tests/test-php-ast.d.ts +0 -1
  119. package/tests/test-php-ast.js +0 -152
  120. package/tests/test-php-ast.js.map +0 -1
  121. package/tests/test-php-ast.ts +0 -155
  122. package/tests/test-php.d.ts +0 -1
  123. package/tests/test-php.js +0 -234
  124. package/tests/test-php.js.map +0 -1
  125. package/tests/test-php.ts +0 -261
  126. package/tests/test-smoke.d.ts +0 -5
  127. package/tests/test-smoke.js +0 -83
  128. package/tests/test-smoke.js.map +0 -1
  129. package/tests/test-smoke.ts +0 -93
  130. package/tests/writers/test-crlf-handling.ts +0 -100
  131. package/tests/writers/test-write-smoke.ts +0 -568
  132. package/tsconfig.json +0 -19
package/src/index.ts DELETED
@@ -1,1784 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * mcp-code-context — Semantic Code Context Server
5
- *
6
- * A Model Context Protocol (MCP) server that compresses any code repository
7
- * into LLM-ready semantic context and provides surgical code editing.
8
- *
9
- * Read Tools:
10
- * 1. get_semantic_repo_map — Compressed structural map of a repo
11
- * 2. read_file_surgical — Full file or specific symbol extraction
12
- * 3. analyze_impact — Find all dependents of a modified file
13
- *
14
- * Write Tools:
15
- * 4. write_file_surgical — Replace a symbol's code in-place
16
- * 5. insert_symbol — Insert code at precise AST positions
17
- * 6. rename_symbol — Rename across entire repository
18
- * 7. remove_symbol — Safely delete symbols with dep checking
19
- *
20
- * Transport: stdio (JSON-RPC over stdin/stdout)
21
- * Compatible with: Claude Desktop, Cursor, Windsurf, Copilot, Amazon Q, and any MCP client.
22
- */
23
-
24
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
25
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
- import {
27
- CallToolRequestSchema,
28
- ListToolsRequestSchema,
29
- } from "@modelcontextprotocol/sdk/types.js";
30
- import { IgnoreManager } from "./utils/ignoreManager.js";
31
- import { compressFile, extractSymbol } from "./ast/semanticCompressor.js";
32
- import {
33
- replaceSymbol,
34
- insertCode,
35
- renameInFile,
36
- renameReferencesInFile,
37
- removeSymbolFromFile,
38
- WRITABLE_EXTENSIONS,
39
- type WriteResult,
40
- type InsertPosition,
41
- } from "./ast/writers/symbolWriter.js";
42
- import { generateDiff, generateMultiFileDiff } from "./utils/diffEngine.js";
43
- import { createBackup, restoreBackup, cleanBackup, cleanAllBackups } from "./utils/backupManager.js";
44
- import { extractFallbackSymbols, findClosestSymbols } from "./utils/fuzzyMatch.js";
45
- import { confirmationCache } from "./utils/confirmationCache.js";
46
- import { readFileLines } from "./tools/readFileLines.js";
47
- import { searchCodePattern } from "./tools/searchCodePattern.js";
48
- import * as fs from "node:fs";
49
- import * as path from "node:path";
50
-
51
- // ─── Constants ──────────────────────────────────────────────────────
52
-
53
- const SERVER_NAME = "mcp-code-context";
54
- const SERVER_VERSION = "2.2.0";
55
-
56
- /** Extensions we consider source code for analysis */
57
- const SOURCE_EXTENSIONS = new Set([
58
- ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
59
- ".py", ".pyi",
60
- ".json", ".yaml", ".yml", ".toml",
61
- ".md", ".txt",
62
- ".css", ".scss", ".less",
63
- ".html", ".vue", ".svelte",
64
- ".rs", ".go", ".java", ".kt", ".dart",
65
- ".c", ".cpp", ".h", ".hpp",
66
- ".rb", ".php", ".swift",
67
- ".sh", ".bash", ".zsh",
68
- ".sql",
69
- ".graphql", ".gql",
70
- ".proto",
71
- ".dockerfile",
72
- ]);
73
-
74
- /** Extensions eligible for import analysis */
75
- const IMPORTABLE_EXTENSIONS = new Set([
76
- ".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs",
77
- ".py", ".pyi",
78
- ".php", ".phtml",
79
- ".dart",
80
- ".vue", ".svelte",
81
- ]);
82
-
83
- // ─── MCP Server Instance ────────────────────────────────────────────
84
-
85
- const server = new Server(
86
- { name: SERVER_NAME, version: SERVER_VERSION },
87
- { capabilities: { tools: {} } },
88
- );
89
-
90
- // ─── Tool Definitions ───────────────────────────────────────────────
91
-
92
- const TOOLS = [
93
- {
94
- name: "get_semantic_repo_map",
95
- description:
96
- "Generates a semantically compressed map of a code repository. " +
97
- "Walks the directory tree respecting .gitignore rules, reads source files, " +
98
- "and extracts only structural signatures (functions, classes, interfaces, types) " +
99
- "while stripping implementation bodies. Returns an XML or Markdown document " +
100
- "optimized for LLM consumption with minimal token usage. " +
101
- "Supports TypeScript, JavaScript, PHP (AST-based), Dart, and Python. " +
102
- "Use this to understand the architecture of a large codebase at a glance.",
103
- inputSchema: {
104
- type: "object" as const,
105
- properties: {
106
- directoryPath: {
107
- type: "string",
108
- description:
109
- "Absolute path to the repository root directory to analyze.",
110
- },
111
- format: {
112
- type: "string",
113
- enum: ["xml", "markdown"],
114
- description:
115
- "Output format for the repo map. 'xml' (default) provides structured data, " +
116
- "'markdown' provides human-readable output.",
117
- },
118
- },
119
- required: ["directoryPath"],
120
- },
121
- },
122
- {
123
- name: "read_file_surgical",
124
- description:
125
- "Reads the complete source code of a file, or surgically extracts only " +
126
- "a specific named symbol (function, class, interface, method, type alias). " +
127
- "When a symbol name is provided, only the full source code block for that " +
128
- "symbol is returned — saving tokens compared to reading the entire file. " +
129
- "Supports TypeScript, JavaScript, PHP, Dart, and Python files for symbol extraction.",
130
- inputSchema: {
131
- type: "object" as const,
132
- properties: {
133
- filePath: {
134
- type: "string",
135
- description: "Absolute path to the source file to read.",
136
- },
137
- symbolName: {
138
- type: "string",
139
- description:
140
- "Optional. Name of a specific function, class, interface, method, " +
141
- "or type alias to extract. If omitted, the entire file is returned.",
142
- },
143
- className: {
144
- type: "string",
145
- description:
146
- "Optional. Name of the class to scope the symbol search to. " +
147
- "Use this when multiple classes in the same file have methods with the same name " +
148
- "(e.g., multiple build() methods in Flutter). If omitted, returns the first match.",
149
- },
150
- },
151
- required: ["filePath"],
152
- },
153
- },
154
- {
155
- name: "rollback_file",
156
- description: "Reverts a file to a previous backup state after a surgical write operation. Keeps up to 5 automatic backups. Specify the number of steps to rollback (1 = most recent).",
157
- inputSchema: {
158
- type: "object" as const,
159
- properties: {
160
- filePath: {
161
- type: "string",
162
- description: "Absolute path to the source file to revert.",
163
- },
164
- steps: {
165
- type: "number",
166
- description: "Number of steps back to revert to (1-5). Defaults to 1 (most recent backup).",
167
- }
168
- },
169
- required: ["filePath"],
170
- },
171
- },
172
- {
173
- name: "clean_backups",
174
- description:
175
- "Removes all backup files for a project. Deletes the entire .mcp-backups directory " +
176
- "at the project root, freeing up disk space and cleaning the working directory. " +
177
- "Use this to clean up after completing a series of edits or when backups are no longer needed.",
178
- inputSchema: {
179
- type: "object" as const,
180
- properties: {
181
- projectRoot: {
182
- type: "string",
183
- description:
184
- "Absolute path to the project root directory. The .mcp-backups folder " +
185
- "at this location will be removed.",
186
- },
187
- },
188
- required: ["projectRoot"],
189
- },
190
- },
191
- {
192
- name: "read_file_lines",
193
- description:
194
- "Reads specific line ranges from a file without loading the entire content. " +
195
- "Supports reading by exact line range (startLine/endLine) or around a pattern match. " +
196
- "More efficient than read_file_surgical when you only need a small fragment of code. " +
197
- "Perfect for debugging, viewing specific code blocks, or extracting context around errors.",
198
- inputSchema: {
199
- type: "object" as const,
200
- properties: {
201
- filePath: {
202
- type: "string",
203
- description: "Absolute path to the source file to read.",
204
- },
205
- startLine: {
206
- type: "number",
207
- description: "Starting line number (1-indexed). Required if using exact range mode.",
208
- },
209
- endLine: {
210
- type: "number",
211
- description: "Ending line number (1-indexed). Required if using exact range mode.",
212
- },
213
- aroundPattern: {
214
- type: "string",
215
- description:
216
- "Search pattern to find in the file. Returns lines around the first match. " +
217
- "Use this mode when you don't know the exact line numbers.",
218
- },
219
- contextLines: {
220
- type: "number",
221
- description:
222
- "Number of lines to include before and after the pattern match. " +
223
- "Defaults to 5. Only used with aroundPattern mode.",
224
- },
225
- },
226
- required: ["filePath"],
227
- },
228
- },
229
- {
230
- name: "search_code_pattern",
231
- description:
232
- "Searches for code patterns across multiple files in a repository. " +
233
- "Returns matches with file paths, line numbers, and optional context lines. " +
234
- "Respects .gitignore rules and allows filtering by file extensions. " +
235
- "More efficient than manual grep when you need structured results with context.",
236
- inputSchema: {
237
- type: "object" as const,
238
- properties: {
239
- rootDir: {
240
- type: "string",
241
- description: "Absolute path to the repository root directory to search.",
242
- },
243
- pattern: {
244
- type: "string",
245
- description:
246
- "Regular expression pattern to search for. Use proper regex escaping " +
247
- "(e.g., 'widget\\\\.height' to match 'widget.height').",
248
- },
249
- fileExtensions: {
250
- type: "array",
251
- items: { type: "string" },
252
- description:
253
- "Array of file extensions to search (e.g., ['.ts', '.dart', '.py']). " +
254
- "Defaults to common code extensions if omitted.",
255
- },
256
- excludeDirs: {
257
- type: "array",
258
- items: { type: "string" },
259
- description:
260
- "Array of directory names to exclude (e.g., ['node_modules', 'dist']). " +
261
- "Defaults to common build/dependency directories.",
262
- },
263
- showContext: {
264
- type: "boolean",
265
- description: "If true, includes surrounding lines for each match. Defaults to true.",
266
- },
267
- contextLines: {
268
- type: "number",
269
- description: "Number of context lines before/after each match. Defaults to 3.",
270
- },
271
- maxResults: {
272
- type: "number",
273
- description: "Maximum number of matches to return. Defaults to 50.",
274
- },
275
- },
276
- required: ["rootDir", "pattern"],
277
- },
278
- },
279
- {
280
- name: "analyze_impact",
281
- description:
282
- "Analyzes the impact of modifying a given file by discovering all other " +
283
- "files in the repository that import or depend on it. Scans for ES import/export, " +
284
- "CommonJS require(), Python import, PHP use/require_once/include, and Dart import statements. " +
285
- "Returns a structured report listing all dependent files and the specific " +
286
- "import statements that create the dependency. Essential for understanding " +
287
- "blast radius before refactoring.",
288
- inputSchema: {
289
- type: "object" as const,
290
- properties: {
291
- filePath: {
292
- type: "string",
293
- description:
294
- "Absolute path to the file being modified (the dependency target).",
295
- },
296
- rootDir: {
297
- type: "string",
298
- description:
299
- "Optional. Absolute path to the repository root. If omitted, " +
300
- "the server walks up from filePath to find the nearest directory " +
301
- "containing package.json, .git, or similar root markers.",
302
- },
303
- },
304
- required: ["filePath"],
305
- },
306
- },
307
- // ─── Write Tools ────────────────────────────────────────────────────
308
- {
309
- name: "write_file_surgical",
310
- description:
311
- "Surgically replaces the full source code of a named symbol (function, class, " +
312
- "method, interface, type alias) in a file. The AI provides ONLY the replacement " +
313
- "code for that specific symbol — the tool handles locating it via AST parsing " +
314
- "and splicing the replacement into the correct position. Supports TypeScript, " +
315
- "JavaScript, PHP, Dart, and Python. Returns a unified diff of the changes. " +
316
- "Supports dry-run mode to preview changes without modifying the file.",
317
- inputSchema: {
318
- type: "object" as const,
319
- properties: {
320
- filePath: {
321
- type: "string",
322
- description: "Absolute path to the source file to modify.",
323
- },
324
- symbolName: {
325
- type: "string",
326
- description:
327
- "Name of the function, class, method, interface, or type alias to replace.",
328
- },
329
- newContent: {
330
- type: "string",
331
- description:
332
- "The complete replacement source code for the symbol, including its " +
333
- "signature and body. Must be syntactically valid for the target language.",
334
- },
335
- className: {
336
- type: "string",
337
- description:
338
- "Optional. Name of the class to scope the symbol search to. " +
339
- "Use when multiple classes have methods with the same name.",
340
- },
341
- confirmationToken: {
342
- type: "string",
343
- description:
344
- "Required for Phase 2. The token provided from the Phase 1 Dry-Run preview.",
345
- },
346
- confirm: {
347
- type: "boolean",
348
- description:
349
- "Required for Phase 2. Set to true to confirm and apply the changes.",
350
- },
351
- createBackup: {
352
- type: "boolean",
353
- description:
354
- "If true, creates a .bak copy of the file before modifying it. " +
355
- "Defaults to false (opt-in).",
356
- },
357
- },
358
- required: ["filePath", "symbolName", "newContent"],
359
- },
360
- },
361
- {
362
- name: "insert_symbol",
363
- description:
364
- "Inserts new code at a precise location relative to an existing symbol in a file. " +
365
- "Can insert before or after a symbol, or inside a class/interface at the start or end. " +
366
- "If no anchor symbol is provided, inserts at the end of the file. " +
367
- "Supports TypeScript, JavaScript, PHP, Dart, and Python. " +
368
- "Automatically matches the surrounding indentation style.",
369
- inputSchema: {
370
- type: "object" as const,
371
- properties: {
372
- filePath: {
373
- type: "string",
374
- description: "Absolute path to the source file to modify.",
375
- },
376
- code: {
377
- type: "string",
378
- description: "The new code to insert. Must be syntactically valid.",
379
- },
380
- anchorSymbol: {
381
- type: "string",
382
- description:
383
- "Name of an existing symbol to position relative to. " +
384
- "If omitted, code is inserted at the end of the file.",
385
- },
386
- position: {
387
- type: "string",
388
- enum: ["before", "after", "inside_start", "inside_end"],
389
- description:
390
- "Where to insert relative to the anchor. 'before' and 'after' insert " +
391
- "outside the symbol. 'inside_start' and 'inside_end' insert as members " +
392
- "inside a class or interface. Defaults to 'after'.",
393
- },
394
- className: {
395
- type: "string",
396
- description:
397
- "Optional. Name of the class to scope the anchor symbol search to. " +
398
- "Use when multiple classes have methods with the same name.",
399
- },
400
- dryRun: {
401
- type: "boolean",
402
- description: "If true, preview changes without modifying the file.",
403
- },
404
- createBackup: {
405
- type: "boolean",
406
- description: "If true, backup the file before modifying.",
407
- },
408
- },
409
- required: ["filePath", "code"],
410
- },
411
- },
412
- {
413
- name: "rename_symbol",
414
- description:
415
- "Renames a symbol across an entire repository. Renames the definition in the " +
416
- "source file AND updates all files that import or reference it. Uses AST-aware " +
417
- "whole-word replacement to avoid false matches. Supports TypeScript, JavaScript, " +
418
- "PHP, Dart, and Python. Returns a multi-file diff showing all changes. " +
419
- "Essential for safe refactoring.",
420
- inputSchema: {
421
- type: "object" as const,
422
- properties: {
423
- filePath: {
424
- type: "string",
425
- description:
426
- "Absolute path to the file where the symbol is defined.",
427
- },
428
- oldName: {
429
- type: "string",
430
- description: "The current name of the symbol.",
431
- },
432
- newName: {
433
- type: "string",
434
- description: "The new name for the symbol.",
435
- },
436
- rootDir: {
437
- type: "string",
438
- description:
439
- "Optional. Absolute path to the repository root. " +
440
- "If omitted, auto-detected from the file path.",
441
- },
442
- confirmationToken: {
443
- type: "string",
444
- description:
445
- "Required for Phase 2. The token provided from the Phase 1 Dry-Run preview.",
446
- },
447
- confirm: {
448
- type: "boolean",
449
- description:
450
- "Required for Phase 2. Set to true to confirm and apply the changes.",
451
- },
452
- createBackup: {
453
- type: "boolean",
454
- description: "If true, backup each modified file before changing it.",
455
- },
456
- },
457
- required: ["filePath", "oldName", "newName"],
458
- },
459
- },
460
- {
461
- name: "remove_symbol",
462
- description:
463
- "Removes a named symbol from a file. By default, checks for dependents first " +
464
- "and refuses to remove if other files import or reference the symbol. " +
465
- "Use force=true to skip the dependency check and remove anyway. " +
466
- "Supports TypeScript, JavaScript, PHP, Dart, and Python. " +
467
- "Returns a diff showing what was removed.",
468
- inputSchema: {
469
- type: "object" as const,
470
- properties: {
471
- filePath: {
472
- type: "string",
473
- description: "Absolute path to the source file.",
474
- },
475
- symbolName: {
476
- type: "string",
477
- description: "Name of the symbol to remove.",
478
- },
479
- className: {
480
- type: "string",
481
- description:
482
- "Optional. Name of the class to scope the symbol search to. " +
483
- "Use when multiple classes have methods with the same name.",
484
- },
485
- force: {
486
- type: "boolean",
487
- description:
488
- "If true, skip the dependency check and remove the symbol even if " +
489
- "other files reference it. Defaults to false.",
490
- },
491
- dryRun: {
492
- type: "boolean",
493
- description: "If true, preview the removal without modifying the file.",
494
- },
495
- createBackup: {
496
- type: "boolean",
497
- description: "If true, backup the file before modifying.",
498
- },
499
- },
500
- required: ["filePath", "symbolName"],
501
- },
502
- },
503
- ];
504
-
505
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
506
- tools: TOOLS,
507
- }));
508
-
509
- // ─── Tool Dispatcher ────────────────────────────────────────────────
510
-
511
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
512
- const { name, arguments: args } = request.params;
513
-
514
- try {
515
- switch (name) {
516
- case "get_semantic_repo_map":
517
- return await handleGetSemanticRepoMap(
518
- args as Record<string, unknown>,
519
- );
520
- case "read_file_surgical":
521
- return await handleReadFileSurgical(
522
- args as Record<string, unknown>,
523
- );
524
- case "analyze_impact":
525
- return await handleAnalyzeImpact(args as Record<string, unknown>);
526
- case "write_file_surgical":
527
- return await handleWriteFileSurgical(
528
- args as Record<string, unknown>,
529
- );
530
- case "insert_symbol":
531
- return await handleInsertSymbol(
532
- args as Record<string, unknown>,
533
- );
534
- case "rename_symbol":
535
- return await handleRenameSymbol(
536
- args as Record<string, unknown>,
537
- );
538
- case "remove_symbol":
539
- return await handleRemoveSymbol(
540
- args as Record<string, unknown>,
541
- );
542
- case "rollback_file":
543
- return await handleRollbackFile(
544
- args as Record<string, unknown>,
545
- );
546
- case "clean_backups":
547
- return await handleCleanBackups(
548
- args as Record<string, unknown>,
549
- );
550
- case "read_file_lines":
551
- return await handleReadFileLines(
552
- args as Record<string, unknown>,
553
- );
554
- case "search_code_pattern":
555
- return await handleSearchCodePattern(
556
- args as Record<string, unknown>,
557
- );
558
- default:
559
- return errorResponse(`Unknown tool: "${name}"`);
560
- }
561
- } catch (error: unknown) {
562
- const message =
563
- error instanceof Error ? error.message : String(error);
564
- return errorResponse(`Unhandled error in tool "${name}": ${message}`);
565
- }
566
- });
567
-
568
- // ─── Tool 1: get_semantic_repo_map ──────────────────────────────────
569
-
570
- export async function handleGetSemanticRepoMap(args: Record<string, unknown>) {
571
- const directoryPath = args.directoryPath as string;
572
- const format = (args.format as string) || "xml";
573
-
574
- if (!directoryPath) {
575
- return errorResponse("Missing required parameter: directoryPath");
576
- }
577
-
578
- const resolvedPath = path.resolve(directoryPath);
579
-
580
- if (!fs.existsSync(resolvedPath)) {
581
- return errorResponse(`Directory not found: ${resolvedPath}`);
582
- }
583
-
584
- const stat = fs.statSync(resolvedPath);
585
- if (!stat.isDirectory()) {
586
- return errorResponse(`Path is not a directory: ${resolvedPath}`);
587
- }
588
-
589
- const ignoreManager = new IgnoreManager(resolvedPath);
590
- const files = ignoreManager.walkDirectory();
591
- const projectName = path.basename(resolvedPath);
592
-
593
- const entries: FileEntry[] = [];
594
- let skippedCount = 0;
595
-
596
- for (const file of files) {
597
- const ext = path.extname(file).toLowerCase();
598
- if (!SOURCE_EXTENSIONS.has(ext)) {
599
- skippedCount++;
600
- continue;
601
- }
602
-
603
- let content: string;
604
- try {
605
- content = fs.readFileSync(file, "utf-8");
606
- } catch {
607
- skippedCount++;
608
- continue;
609
- }
610
-
611
- // Skip files with null bytes (binary masquerading as text)
612
- if (content.includes("\0")) {
613
- skippedCount++;
614
- continue;
615
- }
616
-
617
- const relativePath = path
618
- .relative(resolvedPath, file)
619
- .replace(/\\/g, "/");
620
- const language = getLanguageName(ext);
621
- const compressed = compressFile(file, content);
622
-
623
- entries.push({ relativePath, language, compressed });
624
- }
625
-
626
- const output =
627
- format === "markdown"
628
- ? formatAsMarkdown(projectName, resolvedPath, entries, skippedCount)
629
- : formatAsXml(projectName, resolvedPath, entries, skippedCount);
630
-
631
- return {
632
- content: [{ type: "text" as const, text: output }],
633
- };
634
- }
635
-
636
- // ─── Tool 2: read_file_surgical ─────────────────────────────────────
637
-
638
- export async function handleReadFileSurgical(args: Record<string, unknown>) {
639
- const filePath = args.filePath as string;
640
- const symbolName = args.symbolName as string | undefined;
641
- const className = args.className as string | undefined;
642
-
643
- if (!filePath) {
644
- return errorResponse("Missing required parameter: filePath");
645
- }
646
-
647
- const resolvedPath = path.resolve(filePath);
648
-
649
- if (!fs.existsSync(resolvedPath)) {
650
- return errorResponse(`File not found: ${resolvedPath}`);
651
- }
652
-
653
- let content: string;
654
- try {
655
- content = fs.readFileSync(resolvedPath, "utf-8");
656
- } catch (error: unknown) {
657
- const msg = error instanceof Error ? error.message : String(error);
658
- return errorResponse(`Failed to read file: ${msg}`);
659
- }
660
-
661
- // If no symbol requested, return the full file
662
- if (!symbolName) {
663
- return {
664
- content: [
665
- {
666
- type: "text" as const,
667
- text: `// File: ${resolvedPath}\n// Lines: ${content.split("\n").length}\n\n${content}`,
668
- },
669
- ],
670
- };
671
- }
672
-
673
- // Attempt surgical extraction
674
- const extracted = extractSymbol(content, symbolName, resolvedPath, className);
675
-
676
- if (extracted === null) {
677
- // Symbol not found — use fuzzy matching to return suggestions
678
- const available = extractFallbackSymbols(content);
679
- const closest = findClosestSymbols(symbolName, available, 5);
680
-
681
- const availableFormatted = available.slice(0, 15).map(s => `${s.name} (${s.type}${s.className ? ` in ${s.className}` : ''})`);
682
- if (available.length > 15) availableFormatted.push(`...and ${available.length - 15} more`);
683
-
684
- const suggestions = [];
685
- if (className) {
686
- // Check if it exists in another class
687
- const inOtherClass = available.find(s => s.name === symbolName && s.className !== className);
688
- if (inOtherClass) suggestions.push(`Did you mean '${symbolName}' in class '${inOtherClass.className}'? Update your className parameter.`);
689
- } else {
690
- // Check if it requires a class scope
691
- const needsScope = available.filter(s => s.name === symbolName && s.className);
692
- if (needsScope.length > 0) suggestions.push(`Symbol '${symbolName}' exists inside multiple scopes. Did you mean to use className: '${needsScope[0].className}'?`);
693
- }
694
-
695
- if (closest.length > 0) {
696
- suggestions.push(`Similar symbols: ${closest.map(s => s.name).join(', ')}`);
697
- }
698
-
699
- const errorPayload = {
700
- error: `Symbol '${symbolName}' not found in ${path.basename(resolvedPath)}`,
701
- available_symbols: availableFormatted,
702
- suggestions
703
- };
704
-
705
- return {
706
- isError: true,
707
- content: [
708
- {
709
- type: "text" as const,
710
- text: JSON.stringify(errorPayload, null, 2),
711
- },
712
- ],
713
- };
714
- }
715
-
716
- return {
717
- content: [
718
- {
719
- type: "text" as const,
720
- text:
721
- `// Symbol: ${symbolName}\n` +
722
- `// File: ${resolvedPath}\n\n${extracted}`,
723
- },
724
- ],
725
- };
726
- }
727
-
728
- // ─── Tool 3: analyze_impact ─────────────────────────────────────────
729
-
730
- export async function handleAnalyzeImpact(args: Record<string, unknown>) {
731
- const filePath = args.filePath as string;
732
- let rootDir = args.rootDir as string | undefined;
733
-
734
- if (!filePath) {
735
- return errorResponse("Missing required parameter: filePath");
736
- }
737
-
738
- const resolvedPath = path.resolve(filePath);
739
-
740
- if (!fs.existsSync(resolvedPath)) {
741
- return errorResponse(`File not found: ${resolvedPath}`);
742
- }
743
-
744
- // Discover project root if not provided
745
- if (!rootDir) {
746
- rootDir = findProjectRoot(resolvedPath);
747
- }
748
-
749
- if (!rootDir || !fs.existsSync(rootDir)) {
750
- return errorResponse(
751
- `Could not determine project root for ${resolvedPath}. ` +
752
- `Provide rootDir explicitly.`,
753
- );
754
- }
755
-
756
- rootDir = path.resolve(rootDir);
757
-
758
- const ignoreManager = new IgnoreManager(rootDir);
759
- const allFiles = ignoreManager.walkDirectory();
760
-
761
- const targetRelative = path
762
- .relative(rootDir, resolvedPath)
763
- .replace(/\\/g, "/");
764
-
765
- // Import pattern regexes
766
- const importPatterns: RegExp[] = [
767
- // ES static import: import ... from "path"
768
- /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/gm,
769
- // ES dynamic import: import("path")
770
- /import\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
771
- // CommonJS: require("path")
772
- /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm,
773
- // Python: from package import ...
774
- /from\s+(\S+)\s+import/gm,
775
- // Python: import package
776
- /^import\s+(\S+)/gm,
777
- // PHP: use Namespace\Class
778
- /^\s*use\s+([\w\\]+)/gm,
779
- // PHP: require_once / require / include_once / include
780
- /(?:require_once|require|include_once|include)\s*\(?\s*['"]([^'"]+)['"]\s*\)?/gm,
781
- // Dart: import 'package:...' or import '../...';
782
- /^\s*import\s+['"]([^'"]+)['"]\s*;/gm,
783
- ];
784
-
785
- const dependents: Dependent[] = [];
786
-
787
- for (const file of allFiles) {
788
- // Skip the target file itself
789
- if (path.resolve(file) === resolvedPath) continue;
790
-
791
- // Only scan importable file types
792
- const ext = path.extname(file).toLowerCase();
793
- if (!IMPORTABLE_EXTENSIONS.has(ext)) continue;
794
-
795
- let content: string;
796
- try {
797
- content = fs.readFileSync(file, "utf-8");
798
- } catch {
799
- continue;
800
- }
801
-
802
- let isDependent = false;
803
- const matchedImports: string[] = [];
804
-
805
- for (const pattern of importPatterns) {
806
- const regex = new RegExp(pattern.source, pattern.flags);
807
- let match: RegExpExecArray | null;
808
-
809
- while ((match = regex.exec(content)) !== null) {
810
- const importPath = match[1];
811
- if (
812
- resolveImportMatch(importPath, resolvedPath, file, rootDir)
813
- ) {
814
- matchedImports.push(match[0].trim());
815
- isDependent = true;
816
- }
817
- }
818
- }
819
-
820
- if (isDependent) {
821
- dependents.push({
822
- file: path.relative(rootDir, file).replace(/\\/g, "/"),
823
- imports: [...new Set(matchedImports)], // deduplicate
824
- });
825
- }
826
- }
827
-
828
- // Build the report
829
- const report = buildImpactReport(
830
- targetRelative,
831
- rootDir,
832
- dependents,
833
- );
834
-
835
- return {
836
- content: [{ type: "text" as const, text: report }],
837
- };
838
- }
839
-
840
- // ─── Tool 4: write_file_surgical ────────────────────────────────────
841
-
842
- export async function handleWriteFileSurgical(args: Record<string, unknown>) {
843
- const filePath = args.filePath as string;
844
- const symbolName = args.symbolName as string;
845
- const newContent = args.newContent as string;
846
- const className = args.className as string | undefined;
847
- const shouldBackup = (args.createBackup as boolean) || false;
848
-
849
- const confirmationToken = args.confirmationToken as string | undefined;
850
- const confirm = args.confirm as boolean | undefined;
851
-
852
- if (!filePath) return errorResponse("Missing required parameter: filePath");
853
-
854
- // PHASE 2: Apply the change
855
- if (confirmationToken && confirm) {
856
- const pending = confirmationCache.consume(confirmationToken);
857
- if (!pending || pending.operation !== "write_file_surgical") {
858
- return errorResponse("Invalid or expired confirmation token. Please run the operation again to generate a new preview.");
859
- }
860
-
861
- const resolvedPath = pending.filePath;
862
- try {
863
- createBackup(resolvedPath);
864
- } catch (error: unknown) {
865
- return errorResponse(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
866
- }
867
-
868
- try {
869
- fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
870
- } catch (error: unknown) {
871
- restoreBackup(resolvedPath);
872
- return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
873
- }
874
-
875
- return {
876
- content: [
877
- {
878
- type: "text" as const,
879
- text: `// ✅ Symbol "${pending.payload.symbolName}" replaced successfully\n// File: ${resolvedPath}\n\n${pending.payload.diff}`,
880
- },
881
- ],
882
- };
883
- }
884
-
885
- // PHASE 1: Dry-Run Preview
886
- if (!symbolName) return errorResponse("Missing required parameter: symbolName");
887
- if (!newContent) return errorResponse("Missing required parameter: newContent");
888
-
889
- const resolvedPath = path.resolve(filePath);
890
-
891
- if (!fs.existsSync(resolvedPath)) {
892
- return errorResponse(`File not found: ${resolvedPath}`);
893
- }
894
-
895
- const ext = path.extname(resolvedPath).toLowerCase();
896
- if (!WRITABLE_EXTENSIONS.has(ext)) {
897
- return errorResponse(
898
- `Unsupported file type "${ext}" for surgical writing. ` +
899
- `Supported: .ts, .tsx, .js, .jsx, .mts, .mjs, .cts, .cjs, .php, .phtml, .dart, .py, .pyi`
900
- );
901
- }
902
-
903
- let content: string;
904
- try {
905
- content = fs.readFileSync(resolvedPath, "utf-8");
906
- } catch (error: unknown) {
907
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
908
- }
909
-
910
- // Perform the replacement memory-only
911
- const result: WriteResult = replaceSymbol(resolvedPath, content, symbolName, newContent, className);
912
-
913
- if (!result.success) {
914
- const isNotFoundError = result.error && (result.error.includes("not found") || result.error.includes("Could not locate"));
915
- if (isNotFoundError) {
916
- const available = extractFallbackSymbols(content);
917
- const closest = findClosestSymbols(symbolName, available, 5);
918
-
919
- const availableFormatted = available.slice(0, 15).map(s => `${s.name} (${s.type}${s.className ? ` in ${s.className}` : ''})`);
920
- if (available.length > 15) availableFormatted.push(`...and ${available.length - 15} more`);
921
-
922
- const suggestions = [];
923
- if (className) {
924
- const inOtherClass = available.find(s => s.name === symbolName && s.className !== className);
925
- if (inOtherClass) suggestions.push(`Did you mean '${symbolName}' in class '${inOtherClass.className}'? Update your className parameter.`);
926
- } else {
927
- const needsScope = available.filter(s => s.name === symbolName && s.className);
928
- if (needsScope.length > 0) suggestions.push(`Symbol '${symbolName}' exists inside multiple scopes. Did you mean to use className: '${needsScope[0].className}'?`);
929
- }
930
-
931
- if (closest.length > 0) {
932
- suggestions.push(`Similar symbols: ${closest.map(s => s.name).join(', ')}`);
933
- }
934
-
935
- return {
936
- isError: true,
937
- content: [
938
- {
939
- type: "text" as const,
940
- text: JSON.stringify({
941
- error: result.error,
942
- available_symbols: availableFormatted,
943
- suggestions
944
- }, null, 2),
945
- },
946
- ],
947
- };
948
- }
949
- return errorResponse(result.error || "Unknown error during symbol replacement");
950
- }
951
-
952
- const diff = generateDiff(content, result.newContent, resolvedPath);
953
-
954
- const token = confirmationCache.add("write_file_surgical", resolvedPath, {
955
- symbolName,
956
- newContentFull: result.newContent,
957
- diff,
958
- });
959
-
960
- return {
961
- content: [
962
- {
963
- type: "text" as const,
964
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${diff}`,
965
- },
966
- ],
967
- };
968
- }
969
-
970
- // ─── Tool 5: insert_symbol ──────────────────────────────────────────
971
-
972
- export async function handleInsertSymbol(args: Record<string, unknown>) {
973
- const filePath = args.filePath as string;
974
- const code = args.code as string;
975
- const anchorSymbol = (args.anchorSymbol as string) || null;
976
- const position = (args.position as InsertPosition) || "after";
977
- const className = args.className as string | undefined;
978
- const shouldBackup = (args.createBackup as boolean) || false;
979
-
980
- const confirmationToken = args.confirmationToken as string | undefined;
981
- const confirm = args.confirm as boolean | undefined;
982
-
983
- if (!filePath) return errorResponse("Missing required parameter: filePath");
984
-
985
- // PHASE 2: Apply the change
986
- if (confirmationToken && confirm) {
987
- const pending = confirmationCache.consume(confirmationToken);
988
- if (!pending || pending.operation !== "insert_symbol") {
989
- return errorResponse("Invalid or expired confirmation token. Please run the operation again to generate a new preview.");
990
- }
991
-
992
- const resolvedPath = pending.filePath;
993
- try {
994
- createBackup(resolvedPath);
995
- } catch (error: unknown) {
996
- return errorResponse(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
997
- }
998
-
999
- try {
1000
- fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
1001
- } catch (error: unknown) {
1002
- restoreBackup(resolvedPath);
1003
- return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
1004
- }
1005
-
1006
- return {
1007
- content: [
1008
- {
1009
- type: "text" as const,
1010
- text: `// ✅ Code inserted successfully\n// File: ${resolvedPath}\n// Anchor: ${pending.payload.anchorSymbol || "(end of file)"}\n// Position: ${pending.payload.position}\n\n${pending.payload.diff}`,
1011
- },
1012
- ],
1013
- };
1014
- }
1015
-
1016
- // PHASE 1: Dry-Run
1017
- if (!code) return errorResponse("Missing required parameter: code");
1018
-
1019
- const resolvedPath = path.resolve(filePath);
1020
-
1021
- if (!fs.existsSync(resolvedPath)) {
1022
- return errorResponse(`File not found: ${resolvedPath}`);
1023
- }
1024
-
1025
- const ext = path.extname(resolvedPath).toLowerCase();
1026
- if (!WRITABLE_EXTENSIONS.has(ext)) {
1027
- return errorResponse(`Unsupported file type "${ext}" for code insertion.`);
1028
- }
1029
-
1030
- let fileContent: string;
1031
- try {
1032
- fileContent = fs.readFileSync(resolvedPath, "utf-8");
1033
- } catch (error: unknown) {
1034
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
1035
- }
1036
-
1037
- const result: WriteResult = insertCode(resolvedPath, fileContent, code, anchorSymbol, position, className);
1038
-
1039
- if (!result.success) {
1040
- return errorResponse(result.error || "Unknown error during code insertion");
1041
- }
1042
-
1043
- const diff = generateDiff(fileContent, result.newContent, resolvedPath);
1044
-
1045
- const token = confirmationCache.add("insert_symbol", resolvedPath, {
1046
- newContentFull: result.newContent,
1047
- diff,
1048
- anchorSymbol,
1049
- position
1050
- });
1051
-
1052
- return {
1053
- content: [
1054
- {
1055
- type: "text" as const,
1056
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${diff}`,
1057
- },
1058
- ],
1059
- };
1060
- }
1061
-
1062
- // ─── Tool 6: rename_symbol ──────────────────────────────────────────
1063
-
1064
- export async function handleRenameSymbol(args: Record<string, unknown>) {
1065
- const filePath = args.filePath as string;
1066
- const oldName = args.oldName as string;
1067
- const newName = args.newName as string;
1068
- const rootDir = args.rootDir as string | undefined;
1069
- const shouldBackup = (args.createBackup as boolean) || false;
1070
-
1071
- const confirmationToken = args.confirmationToken as string | undefined;
1072
- const confirm = args.confirm as boolean | undefined;
1073
-
1074
- if (!filePath) return errorResponse("Missing required parameter: filePath");
1075
-
1076
- // PHASE 2
1077
- if (confirmationToken && confirm) {
1078
- const pending = confirmationCache.consume(confirmationToken);
1079
- if (!pending || pending.operation !== "rename_symbol") {
1080
- return errorResponse("Invalid or expired confirmation token.");
1081
- }
1082
-
1083
- const results = pending.payload.results;
1084
- for (const res of results) {
1085
- try { createBackup(res.filePath); } catch (e) {}
1086
- }
1087
-
1088
- try {
1089
- for (const res of results) {
1090
- fs.writeFileSync(res.filePath, res.newContent, "utf-8");
1091
- }
1092
- } catch (error: unknown) {
1093
- return errorResponse(`Failed to write files during rename: ${error instanceof Error ? error.message : String(error)}`);
1094
- }
1095
-
1096
- return {
1097
- content: [{
1098
- type: "text" as const,
1099
- text: `// ✅ Rename applied successfully.\n\n${pending.payload.diff}`,
1100
- }],
1101
- };
1102
- }
1103
-
1104
- // PHASE 1
1105
- if (!oldName) return errorResponse("Missing required parameter: oldName");
1106
- if (!newName) return errorResponse("Missing required parameter: newName");
1107
-
1108
- const resolvedPath = path.resolve(filePath);
1109
- if (!fs.existsSync(resolvedPath)) {
1110
- return errorResponse(`File not found: ${resolvedPath}`);
1111
- }
1112
-
1113
- let sourceContent: string;
1114
- try {
1115
- sourceContent = fs.readFileSync(resolvedPath, "utf-8");
1116
- } catch (error: unknown) {
1117
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
1118
- }
1119
-
1120
- // 1. Rename in the source file
1121
- const sourceResult = renameInFile(resolvedPath, sourceContent, oldName, newName);
1122
- if (!sourceResult.success) {
1123
- return errorResponse(sourceResult.error || `Could not find definition for "${oldName}" in ${path.basename(resolvedPath)}.`);
1124
- }
1125
-
1126
- const results: { filePath: string; oldContent: string; newContent: string }[] = [
1127
- {
1128
- filePath: resolvedPath,
1129
- oldContent: sourceContent,
1130
- newContent: sourceResult.newContent,
1131
- },
1132
- ];
1133
-
1134
- // 2. Discover and rename in dependent files
1135
- const projectRoot = rootDir ? path.resolve(rootDir) : findProjectRoot(resolvedPath);
1136
- if (projectRoot) {
1137
- const ignoreManager = new IgnoreManager(projectRoot);
1138
- const allFiles = ignoreManager.walkDirectory();
1139
-
1140
- for (const dependentFile of allFiles) {
1141
- if (path.resolve(dependentFile) === resolvedPath) continue;
1142
-
1143
- const ext = path.extname(dependentFile).toLowerCase();
1144
- if (!IMPORTABLE_EXTENSIONS.has(ext)) continue;
1145
-
1146
- let depContent: string;
1147
- try {
1148
- depContent = fs.readFileSync(dependentFile, "utf-8");
1149
- } catch {
1150
- continue;
1151
- }
1152
-
1153
- // Quick check if file contains oldName before attempting heavy rename
1154
- const escaped = oldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1155
- const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`);
1156
- if (!regex.test(depContent)) continue;
1157
-
1158
- const depResult = renameReferencesInFile(dependentFile, depContent, oldName, newName);
1159
- if (depResult.success && depResult.newContent !== depContent) {
1160
- results.push({
1161
- filePath: path.resolve(dependentFile),
1162
- oldContent: depContent,
1163
- newContent: depResult.newContent,
1164
- });
1165
- }
1166
- }
1167
- }
1168
-
1169
- const multiDiff = generateMultiFileDiff(results);
1170
-
1171
- const token = confirmationCache.add("rename_symbol", resolvedPath, {
1172
- results,
1173
- diff: multiDiff
1174
- });
1175
-
1176
- return {
1177
- content: [{
1178
- type: "text" as const,
1179
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${multiDiff}`,
1180
- }],
1181
- };
1182
- }
1183
-
1184
- // ─── Tool 7: remove_symbol ──────────────────────────────────────────
1185
-
1186
- export async function handleRemoveSymbol(args: Record<string, unknown>) {
1187
- const filePath = args.filePath as string;
1188
- const symbolName = args.symbolName as string;
1189
- const className = args.className as string | undefined;
1190
- const force = (args.force as boolean) || false;
1191
- const shouldBackup = (args.createBackup as boolean) || false;
1192
-
1193
- const confirmationToken = args.confirmationToken as string | undefined;
1194
- const confirm = args.confirm as boolean | undefined;
1195
-
1196
- if (!filePath) return errorResponse("Missing required parameter: filePath");
1197
-
1198
- // PHASE 2
1199
- if (confirmationToken && confirm) {
1200
- const pending = confirmationCache.consume(confirmationToken);
1201
- if (!pending || pending.operation !== "remove_symbol") {
1202
- return errorResponse("Invalid or expired confirmation token.");
1203
- }
1204
-
1205
- const resolvedPath = pending.filePath;
1206
- try { createBackup(resolvedPath); } catch (e) {}
1207
-
1208
- try {
1209
- fs.writeFileSync(resolvedPath, pending.payload.newContentFull, "utf-8");
1210
- } catch (error: unknown) {
1211
- restoreBackup(resolvedPath);
1212
- return errorResponse(`Failed to write file: ${error instanceof Error ? error.message : String(error)}`);
1213
- }
1214
-
1215
- return {
1216
- content: [{
1217
- type: "text" as const,
1218
- text: `// ✅ Symbol "${pending.payload.symbolName}" removed successfully.\n\n${pending.payload.diff}`,
1219
- }],
1220
- };
1221
- }
1222
-
1223
- if (!symbolName) return errorResponse("Missing required parameter: symbolName");
1224
-
1225
- const resolvedPath = path.resolve(filePath);
1226
- if (!fs.existsSync(resolvedPath)) {
1227
- return errorResponse(`File not found: ${resolvedPath}`);
1228
- }
1229
-
1230
- let fileContent: string;
1231
- try {
1232
- fileContent = fs.readFileSync(resolvedPath, "utf-8");
1233
- } catch (error: unknown) {
1234
- return errorResponse(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`);
1235
- }
1236
-
1237
- // Safety check: Don't remove if dependents exist unless forced
1238
- if (!force) {
1239
- const projectRoot = findProjectRoot(resolvedPath);
1240
- if (projectRoot) {
1241
- const ignoreManager = new IgnoreManager(projectRoot);
1242
- const allFiles = ignoreManager.walkDirectory();
1243
- const targetRel = path.relative(projectRoot, resolvedPath).replace(/\\/g, "/");
1244
-
1245
- for (const f of allFiles) {
1246
- if (path.resolve(f) === resolvedPath) continue;
1247
- const fExt = path.extname(f).toLowerCase();
1248
- if (!IMPORTABLE_EXTENSIONS.has(fExt)) continue;
1249
-
1250
- try {
1251
- const fContent = fs.readFileSync(f, "utf-8");
1252
- const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1253
- const regex = new RegExp(`(?<=^|[^a-zA-Z0-9_$])(${escaped})(?=[^a-zA-Z0-9_$]|$)`);
1254
-
1255
- if (regex.test(fContent)) {
1256
- return errorResponse(`Symbol "${symbolName}" might be used in ${path.relative(projectRoot, f)}. Use force: true to delete anyway.`);
1257
- }
1258
- } catch {}
1259
- }
1260
- }
1261
- }
1262
-
1263
- const result = removeSymbolFromFile(resolvedPath, fileContent, symbolName, className);
1264
- if (!result.success) {
1265
- return errorResponse(result.error || "Unknown error during symbol removal");
1266
- }
1267
-
1268
- const diff = generateDiff(fileContent, result.newContent, resolvedPath);
1269
-
1270
- const token = confirmationCache.add("remove_symbol", resolvedPath, {
1271
- symbolName,
1272
- newContentFull: result.newContent,
1273
- diff
1274
- });
1275
-
1276
- return {
1277
- content: [{
1278
- type: "text" as const,
1279
- text: `// 🔍 ACTION REQUIRED (DRY-RUN DEFAULT)\n// To apply these changes, you MUST call this tool again with:\n// confirmationToken: "${token}"\n// confirm: true\n\n${diff}`,
1280
- }],
1281
- };
1282
- }
1283
-
1284
- // ─── Import Resolution Logic ────────────────────────────────────────
1285
-
1286
- function resolveImportMatch(
1287
- importPath: string,
1288
- targetFile: string,
1289
- sourceFile: string,
1290
- rootDir: string,
1291
- ): boolean {
1292
- const normalized = importPath.replace(/\\/g, "/");
1293
- const targetResolved = path.resolve(targetFile);
1294
- const targetWithoutExt = targetResolved.replace(/\.[^.]+$/, "");
1295
- const targetBaseName = path.basename(
1296
- targetFile,
1297
- path.extname(targetFile),
1298
- );
1299
-
1300
- // Relative imports (./foo, ../bar)
1301
- if (normalized.startsWith(".")) {
1302
- const sourceDir = path.dirname(sourceFile);
1303
- const resolved = path.resolve(sourceDir, normalized);
1304
-
1305
- // Try exact match, then with common extensions, then /index variants
1306
- const candidates = [
1307
- resolved,
1308
- resolved + ".ts",
1309
- resolved + ".tsx",
1310
- resolved + ".js",
1311
- resolved + ".jsx",
1312
- resolved + ".mts",
1313
- resolved + ".mjs",
1314
- resolved + ".dart",
1315
- path.join(resolved, "index.ts"),
1316
- path.join(resolved, "index.tsx"),
1317
- path.join(resolved, "index.js"),
1318
- path.join(resolved, "index.mjs"),
1319
- ];
1320
-
1321
- for (const candidate of candidates) {
1322
- if (
1323
- path.resolve(candidate) === targetResolved ||
1324
- path.resolve(candidate) === targetWithoutExt
1325
- ) {
1326
- return true;
1327
- }
1328
- }
1329
- return false;
1330
- }
1331
-
1332
- // Non-relative imports (aliases, packages)
1333
- // Check if the import path ends with the target's relative path or basename
1334
- const targetRelative = path
1335
- .relative(rootDir, targetFile)
1336
- .replace(/\\/g, "/")
1337
- .replace(/\.[^.]+$/, ""); // strip extension
1338
-
1339
- if (
1340
- normalized === targetBaseName ||
1341
- normalized.endsWith("/" + targetBaseName) ||
1342
- normalized === targetRelative ||
1343
- normalized.endsWith(targetRelative)
1344
- ) {
1345
- return true;
1346
- }
1347
-
1348
- // Python-style dotted imports: convert dots to path separators
1349
- const dottedPath = normalized.replace(/\./g, "/");
1350
- if (
1351
- dottedPath === targetBaseName ||
1352
- dottedPath.endsWith("/" + targetBaseName) ||
1353
- dottedPath === targetRelative ||
1354
- dottedPath.endsWith(targetRelative)
1355
- ) {
1356
- return true;
1357
- }
1358
-
1359
- return false;
1360
- }
1361
-
1362
- // ─── Project Root Discovery ─────────────────────────────────────────
1363
-
1364
- function findProjectRoot(filePath: string): string | undefined {
1365
- const ROOT_MARKERS = [
1366
- "package.json",
1367
- ".git",
1368
- "pyproject.toml",
1369
- "setup.py",
1370
- "Cargo.toml",
1371
- "go.mod",
1372
- "pom.xml",
1373
- "build.gradle",
1374
- "CMakeLists.txt",
1375
- ".gitignore",
1376
- "Gemfile",
1377
- "composer.json",
1378
- "pubspec.yaml",
1379
- ];
1380
-
1381
- let current = path.dirname(path.resolve(filePath));
1382
- const root = path.parse(current).root;
1383
-
1384
- while (current !== root) {
1385
- for (const marker of ROOT_MARKERS) {
1386
- if (fs.existsSync(path.join(current, marker))) {
1387
- return current;
1388
- }
1389
- }
1390
- current = path.dirname(current);
1391
- }
1392
-
1393
- return undefined;
1394
- }
1395
-
1396
- // ─── Output Formatting ─────────────────────────────────────────────
1397
-
1398
- interface FileEntry {
1399
- relativePath: string;
1400
- language: string;
1401
- compressed: string;
1402
- }
1403
-
1404
- interface Dependent {
1405
- file: string;
1406
- imports: string[];
1407
- }
1408
-
1409
- function formatAsXml(
1410
- projectName: string,
1411
- projectPath: string,
1412
- entries: FileEntry[],
1413
- skippedCount: number,
1414
- ): string {
1415
- const lines: string[] = [
1416
- `<?xml version="1.0" encoding="UTF-8"?>`,
1417
- `<repository name="${esc(projectName)}" path="${esc(projectPath)}" files="${entries.length}" skipped="${skippedCount}">`,
1418
- ``,
1419
- ` <file_tree>`,
1420
- ];
1421
-
1422
- for (const entry of entries) {
1423
- lines.push(
1424
- ` <file path="${esc(entry.relativePath)}" language="${esc(entry.language)}" />`,
1425
- );
1426
- }
1427
-
1428
- lines.push(` </file_tree>`);
1429
- lines.push(``);
1430
-
1431
- for (const entry of entries) {
1432
- lines.push(
1433
- ` <file path="${esc(entry.relativePath)}" language="${esc(entry.language)}">`,
1434
- );
1435
- lines.push(`<compressed>`);
1436
- lines.push(entry.compressed);
1437
- lines.push(`</compressed>`);
1438
- lines.push(` </file>`);
1439
- lines.push(``);
1440
- }
1441
-
1442
- lines.push(`</repository>`);
1443
- return lines.join("\n");
1444
- }
1445
-
1446
- function formatAsMarkdown(
1447
- projectName: string,
1448
- projectPath: string,
1449
- entries: FileEntry[],
1450
- skippedCount: number,
1451
- ): string {
1452
- const lines: string[] = [
1453
- `# Repository Map: ${projectName}`,
1454
- ``,
1455
- `> **Path:** \`${projectPath}\` `,
1456
- `> **Source files:** ${entries.length} | **Skipped:** ${skippedCount}`,
1457
- ``,
1458
- `## File Tree`,
1459
- ``,
1460
- ];
1461
-
1462
- for (const entry of entries) {
1463
- lines.push(`- \`${entry.relativePath}\` *(${entry.language})*`);
1464
- }
1465
-
1466
- lines.push(``);
1467
- lines.push(`---`);
1468
- lines.push(``);
1469
-
1470
- for (const entry of entries) {
1471
- const langId = getLangId(entry.language);
1472
- lines.push(`### ${entry.relativePath}`);
1473
- lines.push("```" + langId);
1474
- lines.push(entry.compressed);
1475
- lines.push("```");
1476
- lines.push(``);
1477
- }
1478
-
1479
- return lines.join("\n");
1480
- }
1481
-
1482
- function buildImpactReport(
1483
- targetRelative: string,
1484
- rootDir: string,
1485
- dependents: Dependent[],
1486
- ): string {
1487
- const lines: string[] = [
1488
- `# Impact Analysis`,
1489
- ``,
1490
- `## Target`,
1491
- `- **File:** \`${targetRelative}\``,
1492
- `- **Project root:** \`${rootDir}\``,
1493
- `- **Dependent files:** ${dependents.length}`,
1494
- ``,
1495
- ];
1496
-
1497
- if (dependents.length === 0) {
1498
- lines.push(
1499
- `> No files in this repository import or depend on the target file.`,
1500
- );
1501
- lines.push(``);
1502
- lines.push(
1503
- `This could mean the file is a leaf module, an entry point, ` +
1504
- `or uses non-standard import patterns not detected by this analysis.`,
1505
- );
1506
- } else {
1507
- lines.push(`## Dependent Files`);
1508
- lines.push(``);
1509
-
1510
- for (const dep of dependents) {
1511
- lines.push(`### \`${dep.file}\``);
1512
- for (const imp of dep.imports) {
1513
- lines.push(`- \`${imp}\``);
1514
- }
1515
- lines.push(``);
1516
- }
1517
-
1518
- lines.push(`---`);
1519
- lines.push(``);
1520
- lines.push(
1521
- `> **⚠️ Blast radius:** Modifying \`${targetRelative}\` may affect ` +
1522
- `**${dependents.length}** file(s). Review the import statements above ` +
1523
- `to understand what each dependent file consumes from the target.`,
1524
- );
1525
- }
1526
-
1527
- return lines.join("\n");
1528
- }
1529
-
1530
- // ─── Utility ────────────────────────────────────────────────────────
1531
-
1532
- function getLanguageName(ext: string): string {
1533
- const map: Record<string, string> = {
1534
- ".ts": "TypeScript",
1535
- ".tsx": "TypeScript JSX",
1536
- ".mts": "TypeScript",
1537
- ".cts": "TypeScript",
1538
- ".js": "JavaScript",
1539
- ".jsx": "JavaScript JSX",
1540
- ".mjs": "JavaScript",
1541
- ".cjs": "JavaScript",
1542
- ".py": "Python",
1543
- ".pyi": "Python Stub",
1544
- ".json": "JSON",
1545
- ".yaml": "YAML",
1546
- ".yml": "YAML",
1547
- ".toml": "TOML",
1548
- ".md": "Markdown",
1549
- ".txt": "Text",
1550
- ".css": "CSS",
1551
- ".scss": "SCSS",
1552
- ".less": "LESS",
1553
- ".html": "HTML",
1554
- ".vue": "Vue",
1555
- ".svelte": "Svelte",
1556
- ".rs": "Rust",
1557
- ".go": "Go",
1558
- ".java": "Java",
1559
- ".kt": "Kotlin",
1560
- ".c": "C",
1561
- ".cpp": "C++",
1562
- ".h": "C Header",
1563
- ".hpp": "C++ Header",
1564
- ".rb": "Ruby",
1565
- ".php": "PHP",
1566
- ".dart": "Dart",
1567
- ".swift": "Swift",
1568
- ".sh": "Shell",
1569
- ".bash": "Bash",
1570
- ".zsh": "Zsh",
1571
- ".sql": "SQL",
1572
- ".graphql": "GraphQL",
1573
- ".gql": "GraphQL",
1574
- ".proto": "Protocol Buffers",
1575
- ".dockerfile": "Dockerfile",
1576
- };
1577
- return map[ext] || ext.substring(1).toUpperCase();
1578
- }
1579
-
1580
- function getLangId(language: string): string {
1581
- const map: Record<string, string> = {
1582
- TypeScript: "typescript",
1583
- "TypeScript JSX": "tsx",
1584
- JavaScript: "javascript",
1585
- "JavaScript JSX": "jsx",
1586
- Python: "python",
1587
- "Python Stub": "python",
1588
- JSON: "json",
1589
- YAML: "yaml",
1590
- TOML: "toml",
1591
- Markdown: "markdown",
1592
- CSS: "css",
1593
- SCSS: "scss",
1594
- LESS: "less",
1595
- HTML: "html",
1596
- Vue: "vue",
1597
- Svelte: "svelte",
1598
- Rust: "rust",
1599
- Go: "go",
1600
- Java: "java",
1601
- Kotlin: "kotlin",
1602
- C: "c",
1603
- "C++": "cpp",
1604
- Ruby: "ruby",
1605
- PHP: "php",
1606
- Dart: "dart",
1607
- Swift: "swift",
1608
- Shell: "bash",
1609
- SQL: "sql",
1610
- GraphQL: "graphql",
1611
- };
1612
- return map[language] || language.toLowerCase();
1613
- }
1614
-
1615
- function esc(str: string): string {
1616
- return str
1617
- .replace(/&/g, "&amp;")
1618
- .replace(/</g, "&lt;")
1619
- .replace(/>/g, "&gt;")
1620
- .replace(/"/g, "&quot;")
1621
- .replace(/'/g, "&apos;");
1622
- }
1623
-
1624
- export async function handleRollbackFile(args: Record<string, unknown>) {
1625
- const filePath = args.filePath as string;
1626
- const steps = (args.steps as number) || 1;
1627
-
1628
- if (!filePath) return errorResponse("Missing required parameter: filePath");
1629
-
1630
- const resolvedPath = path.resolve(filePath);
1631
-
1632
- if (steps < 1 || steps > 5) {
1633
- return errorResponse("Steps must be between 1 and 5.");
1634
- }
1635
-
1636
- const success = restoreBackup(resolvedPath, steps);
1637
-
1638
- if (!success) {
1639
- return errorResponse(`No backup found for step ${steps} at ${resolvedPath}`);
1640
- }
1641
-
1642
- return {
1643
- content: [{
1644
- type: "text" as const,
1645
- text: `// ⏪ Rollback successful!\n// Reverted ${path.basename(resolvedPath)} to state from ${steps} step(s) ago.\n// File: ${resolvedPath}`,
1646
- }],
1647
- };
1648
- }
1649
-
1650
- export async function handleCleanBackups(args: Record<string, unknown>) {
1651
- const projectRoot = args.projectRoot as string;
1652
-
1653
- if (!projectRoot) return errorResponse("Missing required parameter: projectRoot");
1654
-
1655
- const resolvedPath = path.resolve(projectRoot);
1656
-
1657
- if (!fs.existsSync(resolvedPath)) {
1658
- return errorResponse(`Project root not found: ${resolvedPath}`);
1659
- }
1660
-
1661
- const success = cleanAllBackups(resolvedPath);
1662
-
1663
- if (!success) {
1664
- return {
1665
- content: [{
1666
- type: "text" as const,
1667
- text: `// ℹ️ No backups found\n// No .mcp-backups directory exists at: ${resolvedPath}`,
1668
- }],
1669
- };
1670
- }
1671
-
1672
- return {
1673
- content: [{
1674
- type: "text" as const,
1675
- text: `// 🧹 Cleanup successful!\n// Removed all backups from: ${resolvedPath}/.mcp-backups\n// Your project is now clean.`,
1676
- }],
1677
- };
1678
- }
1679
-
1680
- // ─── Tool: read_file_lines ──────────────────────────────────────────
1681
-
1682
- export async function handleReadFileLines(args: Record<string, unknown>) {
1683
- const result = readFileLines(args as any);
1684
-
1685
- if (!result.success) {
1686
- return errorResponse(result.error || "Unknown error reading file lines");
1687
- }
1688
-
1689
- const { content, lineRange } = result;
1690
- const header = lineRange
1691
- ? `// Lines ${lineRange.start}-${lineRange.end}\n// File: ${args.filePath}\n\n`
1692
- : `// File: ${args.filePath}\n\n`;
1693
-
1694
- return {
1695
- content: [
1696
- {
1697
- type: "text" as const,
1698
- text: header + content,
1699
- },
1700
- ],
1701
- };
1702
- }
1703
-
1704
- // ─── Tool: search_code_pattern ──────────────────────────────────────
1705
-
1706
- export async function handleSearchCodePattern(args: Record<string, unknown>) {
1707
- const result = searchCodePattern(args as any);
1708
-
1709
- if (!result.success) {
1710
- return errorResponse(result.error || "Unknown error searching code");
1711
- }
1712
-
1713
- const { matches, totalMatches } = result;
1714
-
1715
- if (!matches || matches.length === 0) {
1716
- return {
1717
- content: [
1718
- {
1719
- type: "text" as const,
1720
- text: `# Search Results\n\nNo matches found for pattern: "${args.pattern}"`,
1721
- },
1722
- ],
1723
- };
1724
- }
1725
-
1726
- const lines: string[] = [
1727
- `# Search Results`,
1728
- ``,
1729
- `**Pattern:** \`${args.pattern}\``,
1730
- `**Root:** \`${args.rootDir}\``,
1731
- `**Matches:** ${totalMatches} (showing ${matches.length})`,
1732
- ``,
1733
- ];
1734
-
1735
- for (const match of matches) {
1736
- lines.push(`## \`${match.file}\` (line ${match.lineNumber})`);
1737
- lines.push(``);
1738
-
1739
- if (match.context) {
1740
- lines.push("```");
1741
- lines.push(...match.context);
1742
- lines.push("```");
1743
- } else {
1744
- lines.push(`\`\`\`\n${match.line}\n\`\`\``);
1745
- }
1746
-
1747
- lines.push(``);
1748
- }
1749
-
1750
- return {
1751
- content: [
1752
- {
1753
- type: "text" as const,
1754
- text: lines.join("\n"),
1755
- },
1756
- ],
1757
- };
1758
- }
1759
-
1760
- function errorResponse(message: string) {
1761
- return {
1762
- content: [{ type: "text" as const, text: `❌ ${message}` }],
1763
- isError: true,
1764
- };
1765
- }
1766
-
1767
- // ─── Bootstrap ──────────────────────────────────────────────────────
1768
-
1769
- async function main(): Promise<void> {
1770
- const transport = new StdioServerTransport();
1771
- await server.connect(transport);
1772
- console.error(
1773
- `🚀 ${SERVER_NAME} v${SERVER_VERSION} running on stdio transport`,
1774
- );
1775
- }
1776
-
1777
- import { fileURLToPath, pathToFileURL } from "node:url";
1778
-
1779
- if (import.meta.url === pathToFileURL(process.argv[1]).href) {
1780
- main().catch((error) => {
1781
- console.error("Fatal error starting MCP server:", error);
1782
- process.exit(1);
1783
- });
1784
- }