@imenam/refactoring-mcp 2.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 +72 -0
- package/dist/index.js +167 -0
- package/dist/logger.js +37 -0
- package/dist/utils/config.js +11 -0
- package/dist/utils/files.js +96 -0
- package/dist/utils/outline.js +55 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @imenam/refactoring-mcp
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that lets AI agents copy or move blocks of code between files, with optional directory confinement for safety.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
### `move_or_copy_code`
|
|
8
|
+
|
|
9
|
+
Copies or moves a range of lines from a source file to a destination file.
|
|
10
|
+
|
|
11
|
+
| Parameter | Type | Required | Description |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| `sourceFile` | `string` | ✅ | Absolute path to the source file |
|
|
14
|
+
| `startLine` | `number` | ✅ | Start line of the block (1-indexed) |
|
|
15
|
+
| `endLine` | `number` | ✅ | End line of the block, inclusive (1-indexed) |
|
|
16
|
+
| `operationType` | `"copy" \| "cut"` | ✅ | `copy` keeps the source intact, `cut` removes the lines |
|
|
17
|
+
| `destinationFile` | `string` | ✅ | Absolute path to the destination file (created automatically if it doesn't exist) |
|
|
18
|
+
| `destinationLine` | `number` | ❌ | Line at which to insert in the destination (appended at end by default) |
|
|
19
|
+
|
|
20
|
+
### `get_file_outline`
|
|
21
|
+
|
|
22
|
+
Returns the list of top-level symbols in a file (functions, classes, interfaces, types, constants, methods) with their start and end line numbers. Useful for identifying the exact coordinates of a code block before moving it.
|
|
23
|
+
|
|
24
|
+
| Parameter | Type | Required | Description |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| `filePath` | `string` | ✅ | Absolute path to the file to analyse |
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
All file paths must be **absolute**. Relative paths are rejected.
|
|
31
|
+
|
|
32
|
+
To restrict operations to a specific directory, set the `REFACTORING_ALLOWED_DIR` environment variable. Any attempt to access a file outside that directory will be blocked.
|
|
33
|
+
|
|
34
|
+
```env
|
|
35
|
+
# .env
|
|
36
|
+
REFACTORING_ALLOWED_DIR=C:/Users/you/my-project
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g @imenam/refactoring-mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or use it directly with `npx`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"refactoring-mcp": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@imenam/refactoring-mcp"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or point to a local build:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"refactoring-mcp": {
|
|
61
|
+
"command": "node",
|
|
62
|
+
"args": ["/absolute/path/to/refactoring-mcp/dist/index.js"],
|
|
63
|
+
"env": {
|
|
64
|
+
"REFACTORING_ALLOWED_DIR": "/absolute/path/to/your/project"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { setupLogging } from "./logger.js";
|
|
6
|
+
import { executeRefactor } from "./utils/files.js";
|
|
7
|
+
import { getFileOutline } from "./utils/outline.js";
|
|
8
|
+
setupLogging("MASTER");
|
|
9
|
+
const server = new Server({ name: "refactoring-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
10
|
+
const RefactorSchema = z.object({
|
|
11
|
+
sourceFile: z.string().describe("Chemin absolu complet du fichier d'origine"),
|
|
12
|
+
startLine: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.positive()
|
|
16
|
+
.describe("Ligne de début du bloc de code (1-indexé)"),
|
|
17
|
+
endLine: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.positive()
|
|
21
|
+
.describe("Ligne de fin du bloc de code incluse (1-indexé)"),
|
|
22
|
+
operationType: z
|
|
23
|
+
.enum(["copy", "cut"])
|
|
24
|
+
.describe("Type d'opération : copy (copie) ou cut (coupure/déplacement)"),
|
|
25
|
+
destinationFile: z
|
|
26
|
+
.string()
|
|
27
|
+
.describe("Chemin absolu complet du fichier de destination (créé automatiquement si inexistant)"),
|
|
28
|
+
destinationLine: z
|
|
29
|
+
.number()
|
|
30
|
+
.int()
|
|
31
|
+
.positive()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Optionnel : ligne d'insertion dans le fichier de destination (1-indexé). Ajout en fin de fichier par défaut."),
|
|
34
|
+
});
|
|
35
|
+
const OutlineSchema = z.object({
|
|
36
|
+
filePath: z
|
|
37
|
+
.string()
|
|
38
|
+
.describe("Chemin absolu complet du fichier à analyser"),
|
|
39
|
+
});
|
|
40
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
41
|
+
tools: [
|
|
42
|
+
{
|
|
43
|
+
name: "move_or_copy_code",
|
|
44
|
+
description: "Copie ou déplace un bloc de lignes de code d'un fichier source vers un fichier de destination. " +
|
|
45
|
+
"Le fichier de destination (ainsi que ses dossiers parents) est créé automatiquement s'il n'existe pas. " +
|
|
46
|
+
"Tous les chemins doivent être absolus.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
sourceFile: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Chemin absolu complet du fichier d'origine",
|
|
53
|
+
},
|
|
54
|
+
startLine: {
|
|
55
|
+
type: "number",
|
|
56
|
+
description: "Ligne de début du bloc de code (1-indexé)",
|
|
57
|
+
},
|
|
58
|
+
endLine: {
|
|
59
|
+
type: "number",
|
|
60
|
+
description: "Ligne de fin du bloc de code incluse (1-indexé)",
|
|
61
|
+
},
|
|
62
|
+
operationType: {
|
|
63
|
+
type: "string",
|
|
64
|
+
enum: ["copy", "cut"],
|
|
65
|
+
description: "Type d'opération : copy ou cut",
|
|
66
|
+
},
|
|
67
|
+
destinationFile: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Chemin absolu complet du fichier de destination",
|
|
70
|
+
},
|
|
71
|
+
destinationLine: {
|
|
72
|
+
type: "number",
|
|
73
|
+
description: "Optionnel : ligne d'insertion dans la destination (ajout en fin de fichier par défaut)",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: [
|
|
77
|
+
"sourceFile",
|
|
78
|
+
"startLine",
|
|
79
|
+
"endLine",
|
|
80
|
+
"operationType",
|
|
81
|
+
"destinationFile",
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "get_file_outline",
|
|
87
|
+
description: "Retourne la liste des symboles de premier niveau d'un fichier " +
|
|
88
|
+
"(fonctions, classes, interfaces, types, constantes, méthodes) avec leurs numéros de lignes de début et de fin. " +
|
|
89
|
+
"Utile pour identifier les coordonnées précises d'un bloc de code avant de le déplacer. " +
|
|
90
|
+
"Le chemin doit être absolu.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
filePath: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Chemin absolu complet du fichier à analyser",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["filePath"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}));
|
|
104
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
105
|
+
const { name, arguments: args } = request.params;
|
|
106
|
+
if (name === "get_file_outline") {
|
|
107
|
+
try {
|
|
108
|
+
const { filePath } = OutlineSchema.parse(args);
|
|
109
|
+
const symbols = await getFileOutline(filePath);
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: JSON.stringify(symbols, null, 2) }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: `Erreur : ${error.message}`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
isError: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (name === "move_or_copy_code") {
|
|
127
|
+
try {
|
|
128
|
+
const validatedArgs = RefactorSchema.parse(args);
|
|
129
|
+
const result = await executeRefactor(validatedArgs);
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: `Succès : opération "${result.operation}" effectuée.\n` +
|
|
135
|
+
`- Lignes traitées : ${result.linesMoved}\n` +
|
|
136
|
+
`- Fichier source : ${result.sourceLength} lignes après opération\n` +
|
|
137
|
+
`- Fichier destination : ${result.destLength} lignes après opération`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `Erreur : ${error.message}`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: "text", text: `Outil inconnu : ${name}` }],
|
|
156
|
+
isError: true,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
async function main() {
|
|
160
|
+
const transport = new StdioServerTransport();
|
|
161
|
+
await server.connect(transport);
|
|
162
|
+
console.error("Serveur MCP Refactoring Helper démarré en stdio.");
|
|
163
|
+
}
|
|
164
|
+
main().catch((error) => {
|
|
165
|
+
console.error("Erreur critique au démarrage :", error);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const logDir = process.platform === "win32"
|
|
4
|
+
? "C:\\var\\log\\refactoring-mcp"
|
|
5
|
+
: "/var/log/refactoring-mcp";
|
|
6
|
+
const logFile = path.join(logDir, "server.log");
|
|
7
|
+
export function setupLogging(prefix) {
|
|
8
|
+
if (!fs.existsSync(logDir)) {
|
|
9
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
const logToFile = (msg) => {
|
|
12
|
+
const timestamp = new Date().toISOString();
|
|
13
|
+
try {
|
|
14
|
+
fs.appendFileSync(logFile, `[${timestamp}] [PID ${process.pid}] [${prefix}] ${msg}\n`, "utf-8");
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Silently ignore log write failures to avoid crashing the server
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// Rediriger console.log vers stderr pour protéger le flux JSON-RPC MCP
|
|
21
|
+
console.log = (...args) => {
|
|
22
|
+
const msg = args
|
|
23
|
+
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg)))
|
|
24
|
+
.join(" ");
|
|
25
|
+
logToFile(`[INFO] ${msg}`);
|
|
26
|
+
process.stderr.write(msg + "\n");
|
|
27
|
+
};
|
|
28
|
+
// Surcharger console.error pour journalisation automatique
|
|
29
|
+
const originalError = console.error.bind(console);
|
|
30
|
+
console.error = (...args) => {
|
|
31
|
+
const msg = args
|
|
32
|
+
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg)))
|
|
33
|
+
.join(" ");
|
|
34
|
+
logToFile(`[ERROR] ${msg}`);
|
|
35
|
+
originalError(msg);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
5
|
+
const envPath = path.join(rootDir, ".env");
|
|
6
|
+
export function getConfig() {
|
|
7
|
+
dotenv.config({ path: envPath, override: true });
|
|
8
|
+
const rawDir = process.env.REFACTORING_ALLOWED_DIR;
|
|
9
|
+
const allowedDir = rawDir ? path.resolve(rawDir) : null;
|
|
10
|
+
return { allowedDir };
|
|
11
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getConfig } from "./config.js";
|
|
4
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
5
|
+
export async function validateAndResolvePath(filePath) {
|
|
6
|
+
if (!path.isAbsolute(filePath)) {
|
|
7
|
+
throw new Error(`Le chemin doit être absolu : "${filePath}"`);
|
|
8
|
+
}
|
|
9
|
+
const resolved = path.resolve(filePath);
|
|
10
|
+
// Résoudre les symlinks pour éviter les contournements de confinement
|
|
11
|
+
// Si le fichier n'existe pas encore (destination), on résout le dossier parent
|
|
12
|
+
let realResolved;
|
|
13
|
+
try {
|
|
14
|
+
realResolved = await fs.realpath(resolved);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
const parent = path.dirname(resolved);
|
|
18
|
+
try {
|
|
19
|
+
const realParent = await fs.realpath(parent);
|
|
20
|
+
realResolved = path.join(realParent, path.basename(resolved));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Le dossier parent n'existe pas non plus — on conserve le chemin résolu
|
|
24
|
+
realResolved = resolved;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const { allowedDir } = getConfig();
|
|
28
|
+
if (allowedDir) {
|
|
29
|
+
// Normaliser avec séparateur pour éviter les faux positifs (ex: /foo vs /foobar)
|
|
30
|
+
const normalizedResolved = realResolved.endsWith(path.sep)
|
|
31
|
+
? realResolved
|
|
32
|
+
: realResolved + path.sep;
|
|
33
|
+
const normalizedAllowed = allowedDir.endsWith(path.sep)
|
|
34
|
+
? allowedDir
|
|
35
|
+
: allowedDir + path.sep;
|
|
36
|
+
if (!normalizedResolved.startsWith(normalizedAllowed) &&
|
|
37
|
+
realResolved !== allowedDir) {
|
|
38
|
+
throw new Error(`Accès interdit : le chemin "${realResolved}" n'est pas confiné dans le dossier autorisé "${allowedDir}"`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return realResolved;
|
|
42
|
+
}
|
|
43
|
+
export async function checkFileSize(filePath) {
|
|
44
|
+
const stats = await fs.stat(filePath);
|
|
45
|
+
if (stats.size > MAX_FILE_SIZE_BYTES) {
|
|
46
|
+
throw new Error(`Fichier trop volumineux : ${Math.round(stats.size / 1024 / 1024)} MB (maximum ${MAX_FILE_SIZE_BYTES / 1024 / 1024} MB)`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function executeRefactor(params) {
|
|
50
|
+
const sourcePath = await validateAndResolvePath(params.sourceFile);
|
|
51
|
+
const destPath = await validateAndResolvePath(params.destinationFile);
|
|
52
|
+
// Vérifier la taille du fichier source avant lecture
|
|
53
|
+
await checkFileSize(sourcePath);
|
|
54
|
+
// Lire le fichier source
|
|
55
|
+
const sourceContent = await fs.readFile(sourcePath, "utf-8");
|
|
56
|
+
const sourceLines = sourceContent.split(/\r?\n/);
|
|
57
|
+
const { startLine, endLine } = params;
|
|
58
|
+
if (startLine < 1 || endLine > sourceLines.length || startLine > endLine) {
|
|
59
|
+
throw new Error(`Lignes source invalides (demandées: ${startLine}-${endLine}, fichier source de ${sourceLines.length} lignes)`);
|
|
60
|
+
}
|
|
61
|
+
// Extraire le segment
|
|
62
|
+
const segment = sourceLines.slice(startLine - 1, endLine);
|
|
63
|
+
// Si coupure, supprimer les lignes du fichier source
|
|
64
|
+
if (params.operationType === "cut") {
|
|
65
|
+
sourceLines.splice(startLine - 1, endLine - startLine + 1);
|
|
66
|
+
await fs.writeFile(sourcePath, sourceLines.join("\n"), "utf-8");
|
|
67
|
+
}
|
|
68
|
+
// Préparer le fichier de destination (création automatique si inexistant)
|
|
69
|
+
const destDir = path.dirname(destPath);
|
|
70
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
71
|
+
let destLines = [];
|
|
72
|
+
try {
|
|
73
|
+
const destContent = await fs.readFile(destPath, "utf-8");
|
|
74
|
+
destLines = destContent.split(/\r?\n/);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code !== "ENOENT")
|
|
78
|
+
throw error;
|
|
79
|
+
// Fichier inexistant : on part d'un tableau vide
|
|
80
|
+
}
|
|
81
|
+
// Insérer le segment dans la destination
|
|
82
|
+
if (params.destinationLine !== undefined && params.destinationLine > 0) {
|
|
83
|
+
const insertIndex = Math.max(0, Math.min(destLines.length, params.destinationLine - 1));
|
|
84
|
+
destLines.splice(insertIndex, 0, ...segment);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
destLines.push(...segment);
|
|
88
|
+
}
|
|
89
|
+
await fs.writeFile(destPath, destLines.join("\n"), "utf-8");
|
|
90
|
+
return {
|
|
91
|
+
linesMoved: segment.length,
|
|
92
|
+
operation: params.operationType,
|
|
93
|
+
sourceLength: sourceLines.length,
|
|
94
|
+
destLength: destLines.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { validateAndResolvePath, checkFileSize } from "./files.js";
|
|
3
|
+
const SYMBOL_PATTERNS = [
|
|
4
|
+
{
|
|
5
|
+
regex: /^(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+(\w+)/,
|
|
6
|
+
type: "function",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
regex: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
|
|
10
|
+
type: "class",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
regex: /^(?:export\s+)?interface\s+(\w+)/,
|
|
14
|
+
type: "interface",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
regex: /^(?:export\s+)?type\s+(\w+)\s*[=<]/,
|
|
18
|
+
type: "type",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
regex: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*[=:]/,
|
|
22
|
+
type: "const",
|
|
23
|
+
},
|
|
24
|
+
// Méthodes dans une classe (indentées d'au moins 2 espaces/tab)
|
|
25
|
+
// On exclut les mots-clés JS/TS pour éviter les faux positifs (if, for, while, switch, catch, return...)
|
|
26
|
+
{
|
|
27
|
+
regex: /^[ \t]{2,}(?:(?:public|private|protected|static|async|override)\s+)*(?!(if|for|while|switch|catch|return|throw|new|typeof|instanceof|await|yield|delete|void|case|else|do)\b)(\w+)\s*[(<]/,
|
|
28
|
+
type: "method",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
export async function getFileOutline(filePath) {
|
|
32
|
+
const resolved = await validateAndResolvePath(filePath);
|
|
33
|
+
await checkFileSize(resolved);
|
|
34
|
+
const content = await fs.readFile(resolved, "utf-8");
|
|
35
|
+
const lines = content.split(/\r?\n/);
|
|
36
|
+
const symbols = [];
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i];
|
|
39
|
+
for (const { regex, type } of SYMBOL_PATTERNS) {
|
|
40
|
+
const match = line.match(regex);
|
|
41
|
+
if (match && match[1]) {
|
|
42
|
+
// Pour le pattern "method", le groupe capturant est match[2] car match[1] est le lookahead
|
|
43
|
+
const name = match[2] ?? match[1];
|
|
44
|
+
symbols.push({ name, type, startLine: i + 1, endLine: 0 });
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Calculer les lignes de fin : fin du symbole = début du suivant - 1 (ou fin du fichier)
|
|
50
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
51
|
+
symbols[i].endLine =
|
|
52
|
+
i + 1 < symbols.length ? symbols[i + 1].startLine - 1 : lines.length;
|
|
53
|
+
}
|
|
54
|
+
return symbols;
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imenam/refactoring-mcp",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP server to copy or move code blocks between files, with optional directory confinement.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"refactoring-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"release:patch": "npm version patch && npm publish --access public && git push origin main --follow-tags",
|
|
19
|
+
"release:minor": "npm version minor && npm publish --access public && git push origin main --follow-tags",
|
|
20
|
+
"release:major": "npm version major && npm publish --access public && git push origin main --follow-tags"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"refactoring",
|
|
25
|
+
"code",
|
|
26
|
+
"move",
|
|
27
|
+
"copy",
|
|
28
|
+
"model-context-protocol"
|
|
29
|
+
],
|
|
30
|
+
"author": "imenam",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/Matthieu-Pesnot-Pin/refactoring-mcp.git"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
|
+
"dotenv": "^17.4.2",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.9.1",
|
|
46
|
+
"tsx": "^4.22.3",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|