@imenam/refactoring-mcp 2.0.0 → 3.0.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/README.md +15 -0
- package/dist/index.js +84 -0
- package/dist/utils/compare.js +48 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -25,6 +25,21 @@ Returns the list of top-level symbols in a file (functions, classes, interfaces,
|
|
|
25
25
|
|---|---|---|---|
|
|
26
26
|
| `filePath` | `string` | ✅ | Absolute path to the file to analyse |
|
|
27
27
|
|
|
28
|
+
### `compare_code_sections`
|
|
29
|
+
|
|
30
|
+
Compares two code sections from two files (delimited by line numbers) and returns a unified diff in git format. Useful for verifying that a block of code was correctly copied from one file to another. Leading and trailing blank lines in each section are ignored to avoid false positives from positioning differences.
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Required | Description |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `fileA` | `string` | ✅ | Absolute path to the first file |
|
|
35
|
+
| `startLineA` | `number` | ✅ | Start line of the section in file A (1-indexed) |
|
|
36
|
+
| `endLineA` | `number` | ✅ | End line of the section in file A, inclusive (1-indexed) |
|
|
37
|
+
| `fileB` | `string` | ✅ | Absolute path to the second file |
|
|
38
|
+
| `startLineB` | `number` | ✅ | Start line of the section in file B (1-indexed) |
|
|
39
|
+
| `endLineB` | `number` | ✅ | End line of the section in file B, inclusive (1-indexed) |
|
|
40
|
+
|
|
41
|
+
Returns `"Les sections sont identiques."` when no differences are found, or a unified diff with real file line numbers otherwise.
|
|
42
|
+
|
|
28
43
|
## Configuration
|
|
29
44
|
|
|
30
45
|
All file paths must be **absolute**. Relative paths are rejected.
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -5,6 +6,7 @@ import { z } from "zod";
|
|
|
5
6
|
import { setupLogging } from "./logger.js";
|
|
6
7
|
import { executeRefactor } from "./utils/files.js";
|
|
7
8
|
import { getFileOutline } from "./utils/outline.js";
|
|
9
|
+
import { compareCodeSections } from "./utils/compare.js";
|
|
8
10
|
setupLogging("MASTER");
|
|
9
11
|
const server = new Server({ name: "refactoring-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
10
12
|
const RefactorSchema = z.object({
|
|
@@ -37,6 +39,30 @@ const OutlineSchema = z.object({
|
|
|
37
39
|
.string()
|
|
38
40
|
.describe("Chemin absolu complet du fichier à analyser"),
|
|
39
41
|
});
|
|
42
|
+
const CompareSchema = z.object({
|
|
43
|
+
fileA: z.string().describe("Chemin absolu complet du premier fichier"),
|
|
44
|
+
startLineA: z
|
|
45
|
+
.number()
|
|
46
|
+
.int()
|
|
47
|
+
.positive()
|
|
48
|
+
.describe("Ligne de début de la section dans le fichier A (1-indexé)"),
|
|
49
|
+
endLineA: z
|
|
50
|
+
.number()
|
|
51
|
+
.int()
|
|
52
|
+
.positive()
|
|
53
|
+
.describe("Ligne de fin de la section dans le fichier A (1-indexé, incluse)"),
|
|
54
|
+
fileB: z.string().describe("Chemin absolu complet du second fichier"),
|
|
55
|
+
startLineB: z
|
|
56
|
+
.number()
|
|
57
|
+
.int()
|
|
58
|
+
.positive()
|
|
59
|
+
.describe("Ligne de début de la section dans le fichier B (1-indexé)"),
|
|
60
|
+
endLineB: z
|
|
61
|
+
.number()
|
|
62
|
+
.int()
|
|
63
|
+
.positive()
|
|
64
|
+
.describe("Ligne de fin de la section dans le fichier B (1-indexé, incluse)"),
|
|
65
|
+
});
|
|
40
66
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
41
67
|
tools: [
|
|
42
68
|
{
|
|
@@ -99,6 +125,44 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
99
125
|
required: ["filePath"],
|
|
100
126
|
},
|
|
101
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: "compare_code_sections",
|
|
130
|
+
description: "Compare deux sections de code (délimitées par des numéros de lignes) issues de deux fichiers. " +
|
|
131
|
+
"Retourne un diff au format unifié (style git) avec les numéros de lignes réels dans les fichiers. " +
|
|
132
|
+
"Utile pour vérifier qu'un bloc de code a été correctement recopié d'un fichier à l'autre. " +
|
|
133
|
+
"Les lignes vides en début et en fin de chaque section sont ignorées pour éviter les faux positifs. " +
|
|
134
|
+
"Tous les chemins doivent être absolus.",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
fileA: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "Chemin absolu complet du premier fichier",
|
|
141
|
+
},
|
|
142
|
+
startLineA: {
|
|
143
|
+
type: "number",
|
|
144
|
+
description: "Ligne de début de la section dans le fichier A (1-indexé)",
|
|
145
|
+
},
|
|
146
|
+
endLineA: {
|
|
147
|
+
type: "number",
|
|
148
|
+
description: "Ligne de fin de la section dans le fichier A (1-indexé, incluse)",
|
|
149
|
+
},
|
|
150
|
+
fileB: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Chemin absolu complet du second fichier",
|
|
153
|
+
},
|
|
154
|
+
startLineB: {
|
|
155
|
+
type: "number",
|
|
156
|
+
description: "Ligne de début de la section dans le fichier B (1-indexé)",
|
|
157
|
+
},
|
|
158
|
+
endLineB: {
|
|
159
|
+
type: "number",
|
|
160
|
+
description: "Ligne de fin de la section dans le fichier B (1-indexé, incluse)",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["fileA", "startLineA", "endLineA", "fileB", "startLineB", "endLineB"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
102
166
|
],
|
|
103
167
|
}));
|
|
104
168
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -151,6 +215,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
151
215
|
};
|
|
152
216
|
}
|
|
153
217
|
}
|
|
218
|
+
if (name === "compare_code_sections") {
|
|
219
|
+
try {
|
|
220
|
+
const validatedArgs = CompareSchema.parse(args);
|
|
221
|
+
const result = await compareCodeSections(validatedArgs);
|
|
222
|
+
return {
|
|
223
|
+
content: [{ type: "text", text: result }],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `Erreur : ${error.message}`,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
isError: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
154
238
|
return {
|
|
155
239
|
content: [{ type: "text", text: `Outil inconnu : ${name}` }],
|
|
156
240
|
isError: true,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { createTwoFilesPatch } from "diff";
|
|
3
|
+
import { validateAndResolvePath, checkFileSize } from "./files.js";
|
|
4
|
+
function trimBlankLines(lines) {
|
|
5
|
+
let start = 0;
|
|
6
|
+
while (start < lines.length && lines[start].trim() === "")
|
|
7
|
+
start++;
|
|
8
|
+
let end = lines.length - 1;
|
|
9
|
+
while (end > start && lines[end].trim() === "")
|
|
10
|
+
end--;
|
|
11
|
+
return lines.slice(start, end + 1);
|
|
12
|
+
}
|
|
13
|
+
function adjustDiffLineNumbers(patch, offsetA, offsetB) {
|
|
14
|
+
return patch.replace(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/g, (_, a1, a2, b1, b2) => `@@ -${parseInt(a1) + offsetA},${a2} +${parseInt(b1) + offsetB},${b2} @@`);
|
|
15
|
+
}
|
|
16
|
+
export async function compareCodeSections(params) {
|
|
17
|
+
const pathA = await validateAndResolvePath(params.fileA);
|
|
18
|
+
const pathB = await validateAndResolvePath(params.fileB);
|
|
19
|
+
await checkFileSize(pathA);
|
|
20
|
+
await checkFileSize(pathB);
|
|
21
|
+
const contentA = await fs.readFile(pathA, "utf-8");
|
|
22
|
+
const contentB = await fs.readFile(pathB, "utf-8");
|
|
23
|
+
const linesA = contentA.split(/\r?\n/);
|
|
24
|
+
const linesB = contentB.split(/\r?\n/);
|
|
25
|
+
const { startLineA, endLineA, startLineB, endLineB } = params;
|
|
26
|
+
if (startLineA < 1 || endLineA > linesA.length || startLineA > endLineA) {
|
|
27
|
+
throw new Error(`Lignes invalides pour le fichier A (demandées: ${startLineA}-${endLineA}, fichier de ${linesA.length} lignes)`);
|
|
28
|
+
}
|
|
29
|
+
if (startLineB < 1 || endLineB > linesB.length || startLineB > endLineB) {
|
|
30
|
+
throw new Error(`Lignes invalides pour le fichier B (demandées: ${startLineB}-${endLineB}, fichier de ${linesB.length} lignes)`);
|
|
31
|
+
}
|
|
32
|
+
const rawSectionA = linesA.slice(startLineA - 1, endLineA);
|
|
33
|
+
const rawSectionB = linesB.slice(startLineB - 1, endLineB);
|
|
34
|
+
const sectionA = trimBlankLines(rawSectionA);
|
|
35
|
+
const sectionB = trimBlankLines(rawSectionB);
|
|
36
|
+
// Offset = number of blank lines trimmed from the start of each section
|
|
37
|
+
const trimOffsetA = rawSectionA.indexOf(sectionA[0] ?? "");
|
|
38
|
+
const trimOffsetB = rawSectionB.indexOf(sectionB[0] ?? "");
|
|
39
|
+
const textA = sectionA.join("\n") + "\n";
|
|
40
|
+
const textB = sectionB.join("\n") + "\n";
|
|
41
|
+
if (textA === textB) {
|
|
42
|
+
return "Les sections sont identiques.";
|
|
43
|
+
}
|
|
44
|
+
const rawPatch = createTwoFilesPatch(`${params.fileA} (lignes ${startLineA}-${endLineA})`, `${params.fileB} (lignes ${startLineB}-${endLineB})`, textA, textB);
|
|
45
|
+
const absoluteOffsetA = startLineA - 1 + trimOffsetA;
|
|
46
|
+
const absoluteOffsetB = startLineB - 1 + trimOffsetB;
|
|
47
|
+
return adjustDiffLineNumbers(rawPatch, absoluteOffsetA, absoluteOffsetB);
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/refactoring-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
16
|
"dev": "tsx src/index.ts",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
|
-
"release:patch": "npm version patch && npm publish --access public && git push origin
|
|
19
|
-
"release:minor": "npm version minor && npm publish --access public && git push origin
|
|
20
|
-
"release:major": "npm version major && npm publish --access public && git push origin
|
|
18
|
+
"release:patch": "npm version patch && npm publish --access public && git push origin master --follow-tags",
|
|
19
|
+
"release:minor": "npm version minor && npm publish --access public && git push origin master --follow-tags",
|
|
20
|
+
"release:major": "npm version major && npm publish --access public && git push origin master --follow-tags"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"mcp",
|
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
|
+
"diff": "^9.0.0",
|
|
41
42
|
"dotenv": "^17.4.2",
|
|
42
43
|
"zod": "^4.4.3"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@types/diff": "^7.0.2",
|
|
45
47
|
"@types/node": "^25.9.1",
|
|
46
48
|
"tsx": "^4.22.3",
|
|
47
49
|
"typescript": "^6.0.3"
|