@imenam/refactoring-mcp 3.1.1 → 3.3.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.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
6
  import { setupLogging } from "./logger.js";
7
- import { executeRefactor } from "./utils/files.js";
7
+ import { executeRefactor, insertTextAtLine } from "./utils/files.js";
8
8
  import { getFileOutline } from "./utils/outline.js";
9
9
  import { compareCodeSections } from "./utils/compare.js";
10
10
  setupLogging("MASTER");
@@ -57,6 +57,24 @@ const OutlineSchema = z.object({
57
57
  .optional()
58
58
  .describe("Optionnel : dernière ligne à analyser (1-indexé). Utile pour cibler une section du fichier. Par défaut : fin du fichier."),
59
59
  });
60
+ const InsertTextSchema = z.object({
61
+ filePath: z
62
+ .string()
63
+ .describe("Chemin absolu complet du fichier cible"),
64
+ insertions: z
65
+ .array(z.object({
66
+ lineNumber: z
67
+ .number()
68
+ .int()
69
+ .positive()
70
+ .describe("Numéro de la ligne devant laquelle le texte sera inséré (1-indexé). Utiliser fileLength + 1 pour insérer en fin de fichier."),
71
+ text: z
72
+ .string()
73
+ .describe("Texte à insérer. Peut contenir plusieurs lignes (\\n)."),
74
+ }))
75
+ .min(1)
76
+ .describe("Liste des insertions à effectuer. Les insertions sont appliquées du bas vers le haut pour éviter les décalages d'offset. Les numéros de lignes doivent référencer le fichier dans son état ORIGINAL (avant toute insertion)."),
77
+ });
60
78
  const CompareSchema = z.object({
61
79
  fileA: z.string().describe("Chemin absolu complet du premier fichier"),
62
80
  startLineA: z
@@ -83,6 +101,44 @@ const CompareSchema = z.object({
83
101
  });
84
102
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
85
103
  tools: [
104
+ {
105
+ name: "insert_text_at_line",
106
+ description: "Insère du texte (potentiellement multiligne) à une ou plusieurs lignes données dans un fichier. " +
107
+ "Accepte un tableau d'insertions { lineNumber, text }. " +
108
+ "Les insertions sont appliquées du bas vers le haut pour éviter les décalages d'offset. " +
109
+ "Les numéros de lignes doivent référencer le fichier dans son état original (avant toute insertion). " +
110
+ "Retourne le nombre total de lignes insérées. " +
111
+ "Le chemin doit être absolu.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ filePath: {
116
+ type: "string",
117
+ description: "Chemin absolu complet du fichier cible",
118
+ },
119
+ insertions: {
120
+ type: "array",
121
+ description: "Liste des insertions à effectuer. Les numéros de lignes référencent le fichier dans son état original.",
122
+ items: {
123
+ type: "object",
124
+ properties: {
125
+ lineNumber: {
126
+ type: "number",
127
+ description: "Numéro de la ligne devant laquelle le texte sera inséré (1-indexé). Utiliser fileLength + 1 pour insérer en fin de fichier.",
128
+ },
129
+ text: {
130
+ type: "string",
131
+ description: "Texte à insérer. Peut contenir plusieurs lignes (\\n).",
132
+ },
133
+ },
134
+ required: ["lineNumber", "text"],
135
+ },
136
+ minItems: 1,
137
+ },
138
+ },
139
+ required: ["filePath", "insertions"],
140
+ },
141
+ },
86
142
  {
87
143
  name: "move_or_copy_code",
88
144
  description: "Copie ou déplace un bloc de lignes de code d'un fichier source vers un fichier de destination. " +
@@ -130,6 +186,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
130
186
  name: "get_file_outline",
131
187
  description: "Retourne la liste des symboles d'un fichier (fonctions, classes, interfaces, types, constantes, méthodes) " +
132
188
  "avec leurs numéros de lignes de début et de fin, et leur profondeur d'imbrication (depth). " +
189
+ "Si un bloc de commentaire JSDoc (/** ... */) précède immédiatement un symbole, les champs commentStartLine et commentEndLine sont inclus dans la réponse. " +
133
190
  "Supporte tous les langages à accolades (TypeScript, JavaScript, PHP, Java, C#…). " +
134
191
  "Paramètres optionnels : depth pour filtrer par niveau d'imbrication, startLine/endLine pour cibler une section. " +
135
192
  "Le chemin doit être absolu.",
@@ -218,6 +275,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
218
275
  };
219
276
  }
220
277
  }
278
+ if (name === "insert_text_at_line") {
279
+ try {
280
+ const validatedArgs = InsertTextSchema.parse(args);
281
+ const result = await insertTextAtLine(validatedArgs);
282
+ return {
283
+ content: [
284
+ {
285
+ type: "text",
286
+ text: `Succès : ${result.linesInserted} ligne(s) insérée(s) (${validatedArgs.insertions.length} insertion(s)).\n` +
287
+ `- Fichier : ${result.fileLength} lignes après insertion`,
288
+ },
289
+ ],
290
+ };
291
+ }
292
+ catch (error) {
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: `Erreur : ${error.message}`,
298
+ },
299
+ ],
300
+ isError: true,
301
+ };
302
+ }
303
+ }
221
304
  if (name === "move_or_copy_code") {
222
305
  try {
223
306
  const validatedArgs = RefactorSchema.parse(args);
@@ -1,8 +1,11 @@
1
+ // TEST INSERT A - début du fichier
1
2
  import path from "path";
2
3
  import { fileURLToPath } from "url";
3
4
  import dotenv from "dotenv";
4
5
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
5
6
  const envPath = path.join(rootDir, ".env");
7
+ // TEST INSERT B - avant getConfig
8
+ // ligne 2 du commentaire B
6
9
  export function getConfig() {
7
10
  dotenv.config({ path: envPath, override: true });
8
11
  const rawDir = process.env.REFACTORING_ALLOWED_DIR;
@@ -94,3 +94,28 @@ export async function executeRefactor(params) {
94
94
  destLength: destLines.length,
95
95
  };
96
96
  }
97
+ export async function insertTextAtLine(params) {
98
+ const resolvedPath = await validateAndResolvePath(params.filePath);
99
+ await checkFileSize(resolvedPath);
100
+ const content = await fs.readFile(resolvedPath, "utf-8");
101
+ const fileLines = content.split(/\r?\n/);
102
+ const originalLength = fileLines.length;
103
+ for (const insertion of params.insertions) {
104
+ if (insertion.lineNumber < 1 || insertion.lineNumber > originalLength + 1) {
105
+ throw new Error(`Numéro de ligne invalide (demandé: ${insertion.lineNumber}, fichier de ${originalLength} lignes — valeurs acceptées : 1 à ${originalLength + 1})`);
106
+ }
107
+ }
108
+ // Tri du bas vers le haut pour éviter les décalages d'offset
109
+ const sorted = [...params.insertions].sort((a, b) => b.lineNumber - a.lineNumber);
110
+ let totalLinesInserted = 0;
111
+ for (const insertion of sorted) {
112
+ const newLines = insertion.text.split(/\r?\n/);
113
+ fileLines.splice(insertion.lineNumber - 1, 0, ...newLines);
114
+ totalLinesInserted += newLines.length;
115
+ }
116
+ await fs.writeFile(resolvedPath, fileLines.join("\n"), "utf-8");
117
+ return {
118
+ linesInserted: totalLinesInserted,
119
+ fileLength: fileLines.length,
120
+ };
121
+ }
@@ -33,6 +33,26 @@ const SYMBOL_PATTERNS = [
33
33
  type: "method",
34
34
  },
35
35
  ];
36
+ function findPrecedingComment(lines, symbolLineIndex) {
37
+ let idx = symbolLineIndex - 1;
38
+ while (idx >= 0 && lines[idx].trim() === "") {
39
+ idx--;
40
+ }
41
+ if (idx < 0)
42
+ return null;
43
+ const trimmed = lines[idx].trim();
44
+ if (!trimmed.endsWith("*/"))
45
+ return null;
46
+ const commentEndLine = idx + 1;
47
+ while (idx >= 0) {
48
+ const t = lines[idx].trim();
49
+ if (t.startsWith("/**") || t.startsWith("/*")) {
50
+ return { commentStartLine: idx + 1, commentEndLine };
51
+ }
52
+ idx--;
53
+ }
54
+ return null;
55
+ }
36
56
  function countBracesInLine(line) {
37
57
  let count = 0;
38
58
  let inSingleQuote = false;
@@ -78,7 +98,15 @@ export async function getFileOutline(filePath, options = {}) {
78
98
  // Pour le pattern TS method, le groupe capturant est match[2] (match[1] = lookahead interne)
79
99
  const name = match[2] ?? match[1];
80
100
  if (name) {
81
- symbols.push({ name, type, startLine: lineNum, endLine: 0, depth: braceDepth });
101
+ const comment = findPrecedingComment(lines, i);
102
+ symbols.push({
103
+ name,
104
+ type,
105
+ ...(comment ?? {}),
106
+ startLine: lineNum,
107
+ endLine: 0,
108
+ depth: braceDepth,
109
+ });
82
110
  break;
83
111
  }
84
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/refactoring-mcp",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "MCP server to copy or move code blocks between files, with optional directory confinement.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",