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