mcp-code-context 3.6.3 → 3.7.1
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 +149 -278
- package/INSTRUCTIONS.md +23 -9
- package/README.md +217 -97
- package/SECURITY.md +55 -21
- package/dist/src/core/cacheManager.d.ts +4 -5
- package/dist/src/core/cacheManager.js +49 -33
- package/dist/src/core/cacheManager.js.map +1 -1
- package/dist/src/core/engine.d.ts +1 -1
- package/dist/src/core/engine.js +3 -3
- package/dist/src/core/engine.js.map +1 -1
- package/dist/src/core/indexManager.d.ts +58 -0
- package/dist/src/core/indexManager.js +252 -0
- package/dist/src/core/indexManager.js.map +1 -0
- package/dist/src/core/indexManagerQueries.d.ts +14 -0
- package/dist/src/core/indexManagerQueries.js +95 -0
- package/dist/src/core/indexManagerQueries.js.map +1 -0
- package/dist/src/core/sessionManager.d.ts +117 -0
- package/dist/src/core/sessionManager.js +231 -0
- package/dist/src/core/sessionManager.js.map +1 -0
- package/dist/src/core/validator.d.ts +1 -1
- package/dist/src/core/validator.js +18 -10
- package/dist/src/core/validator.js.map +1 -1
- package/dist/src/index.d.ts +1 -13
- package/dist/src/index.js +25 -999
- package/dist/src/index.js.map +1 -1
- package/dist/src/operations/astTransform.d.ts +38 -0
- package/dist/src/operations/astTransform.js +239 -0
- package/dist/src/operations/astTransform.js.map +1 -0
- package/dist/src/operations/compress.d.ts +5 -3
- package/dist/src/operations/compress.js +73 -7
- package/dist/src/operations/compress.js.map +1 -1
- package/dist/src/operations/confirmationStore.d.ts +35 -8
- package/dist/src/operations/confirmationStore.js +112 -14
- package/dist/src/operations/confirmationStore.js.map +1 -1
- package/dist/src/operations/impactAnalysis.d.ts +9 -0
- package/dist/src/operations/impactAnalysis.js +61 -0
- package/dist/src/operations/impactAnalysis.js.map +1 -0
- package/dist/src/operations/pendingStore.d.ts +51 -0
- package/dist/src/operations/pendingStore.js +201 -0
- package/dist/src/operations/pendingStore.js.map +1 -0
- package/dist/src/operations/pendingStoreInit.d.ts +6 -0
- package/dist/src/operations/pendingStoreInit.js +42 -0
- package/dist/src/operations/pendingStoreInit.js.map +1 -0
- package/dist/src/operations/read.d.ts +6 -92
- package/dist/src/operations/read.js +4 -449
- package/dist/src/operations/read.js.map +1 -1
- package/dist/src/operations/readCore.d.ts +33 -0
- package/dist/src/operations/readCore.js +142 -0
- package/dist/src/operations/readCore.js.map +1 -0
- package/dist/src/operations/rename.d.ts +13 -0
- package/dist/src/operations/rename.js +130 -0
- package/dist/src/operations/rename.js.map +1 -0
- package/dist/src/operations/searchOperation.d.ts +16 -0
- package/dist/src/operations/searchOperation.js +113 -0
- package/dist/src/operations/searchOperation.js.map +1 -0
- package/dist/src/operations/symbolQuery.d.ts +38 -0
- package/dist/src/operations/symbolQuery.js +194 -0
- package/dist/src/operations/symbolQuery.js.map +1 -0
- package/dist/src/operations/write.d.ts +11 -14
- package/dist/src/operations/write.js +6 -125
- package/dist/src/operations/write.js.map +1 -1
- package/dist/src/parsers/base.d.ts +1 -1
- package/dist/src/parsers/base.js +1 -1
- package/dist/src/parsers/csharp.d.ts +12 -0
- package/dist/src/parsers/csharp.js +67 -0
- package/dist/src/parsers/csharp.js.map +1 -0
- package/dist/src/parsers/dartTreeSitter.d.ts +1 -1
- package/dist/src/parsers/dartTreeSitter.js +1 -1
- package/dist/src/parsers/go.d.ts +11 -0
- package/dist/src/parsers/go.js +74 -0
- package/dist/src/parsers/go.js.map +1 -0
- package/dist/src/parsers/java.d.ts +12 -0
- package/dist/src/parsers/java.js +62 -0
- package/dist/src/parsers/java.js.map +1 -0
- package/dist/src/parsers/kotlin.d.ts +12 -0
- package/dist/src/parsers/kotlin.js +75 -0
- package/dist/src/parsers/kotlin.js.map +1 -0
- package/dist/src/parsers/php.d.ts +1 -1
- package/dist/src/parsers/php.js +1 -1
- package/dist/src/parsers/python.d.ts +1 -1
- package/dist/src/parsers/python.js +1 -1
- package/dist/src/parsers/registry.d.ts +1 -1
- package/dist/src/parsers/registry.js +63 -2
- package/dist/src/parsers/registry.js.map +1 -1
- package/dist/src/parsers/ruby.d.ts +12 -0
- package/dist/src/parsers/ruby.js +73 -0
- package/dist/src/parsers/ruby.js.map +1 -0
- package/dist/src/parsers/rust.d.ts +12 -0
- package/dist/src/parsers/rust.js +66 -0
- package/dist/src/parsers/rust.js.map +1 -0
- package/dist/src/parsers/typescript.d.ts +1 -1
- package/dist/src/parsers/typescript.js +28 -38
- package/dist/src/parsers/typescript.js.map +1 -1
- package/dist/src/tools/adminHandlers.d.ts +67 -0
- package/dist/src/tools/adminHandlers.js +129 -0
- package/dist/src/tools/adminHandlers.js.map +1 -0
- package/dist/src/tools/context.d.ts +9 -0
- package/dist/src/tools/context.js +21 -0
- package/dist/src/tools/context.js.map +1 -0
- package/dist/src/tools/handlers.d.ts +4 -0
- package/dist/src/tools/handlers.js +5 -0
- package/dist/src/tools/handlers.js.map +1 -0
- package/dist/src/tools/pipeline.d.ts +6 -0
- package/dist/src/tools/pipeline.js +100 -0
- package/dist/src/tools/pipeline.js.map +1 -0
- package/dist/src/tools/readHandlers.d.ts +55 -0
- package/dist/src/tools/readHandlers.js +220 -0
- package/dist/src/tools/readHandlers.js.map +1 -0
- package/dist/src/tools/toolDefinitions.d.ts +1037 -0
- package/dist/src/tools/toolDefinitions.js +13 -0
- package/dist/src/tools/toolDefinitions.js.map +1 -0
- package/dist/src/tools/toolDefinitionsAdmin.d.ts +89 -0
- package/dist/src/tools/toolDefinitionsAdmin.js +99 -0
- package/dist/src/tools/toolDefinitionsAdmin.js.map +1 -0
- package/dist/src/tools/toolDefinitionsCore.d.ts +949 -0
- package/dist/src/tools/toolDefinitionsCore.js +267 -0
- package/dist/src/tools/toolDefinitionsCore.js.map +1 -0
- package/dist/src/tools/writeHandlers.d.ts +37 -0
- package/dist/src/tools/writeHandlers.js +199 -0
- package/dist/src/tools/writeHandlers.js.map +1 -0
- package/dist/src/utils/appDir.d.ts +19 -0
- package/dist/src/utils/appDir.js +50 -0
- package/dist/src/utils/appDir.js.map +1 -0
- package/dist/src/utils/auditLogWriter.d.ts +9 -0
- package/dist/src/utils/auditLogWriter.js +49 -0
- package/dist/src/utils/auditLogWriter.js.map +1 -0
- package/dist/src/utils/auditLogger.d.ts +1 -10
- package/dist/src/utils/auditLogger.js +14 -109
- package/dist/src/utils/auditLogger.js.map +1 -1
- package/dist/src/utils/auditQuery.d.ts +23 -0
- package/dist/src/utils/auditQuery.js +67 -0
- package/dist/src/utils/auditQuery.js.map +1 -0
- package/dist/src/utils/backupManager.d.ts +2 -2
- package/dist/src/utils/backupManager.js +29 -12
- package/dist/src/utils/backupManager.js.map +1 -1
- package/dist/src/utils/constants.d.ts +1 -1
- package/dist/src/utils/constants.js +8 -1
- package/dist/src/utils/constants.js.map +1 -1
- package/dist/src/utils/diff.d.ts +12 -1
- package/dist/src/utils/diff.js +41 -1
- package/dist/src/utils/diff.js.map +1 -1
- package/dist/src/utils/fileLock.d.ts +7 -2
- package/dist/src/utils/fileLock.js +14 -11
- package/dist/src/utils/fileLock.js.map +1 -1
- package/dist/src/utils/fileWalker.d.ts +1 -1
- package/dist/src/utils/fileWalker.js +1 -1
- package/dist/src/utils/fileWatcher.d.ts +1 -1
- package/dist/src/utils/fileWatcher.js +1 -1
- package/dist/src/utils/fuzzySearch.d.ts +1 -1
- package/dist/src/utils/fuzzySearch.js +1 -1
- package/dist/src/utils/logger.d.ts +1 -1
- package/dist/src/utils/logger.js +2 -2
- package/dist/src/utils/logger.js.map +1 -1
- package/dist/src/utils/rateLimiter.d.ts +9 -1
- package/dist/src/utils/rateLimiter.js +12 -1
- package/dist/src/utils/rateLimiter.js.map +1 -1
- package/dist/src/utils/regexValidator.d.ts +12 -0
- package/dist/src/utils/regexValidator.js +48 -0
- package/dist/src/utils/regexValidator.js.map +1 -0
- package/dist/src/utils/safeRegex.d.ts +2 -92
- package/dist/src/utils/safeRegex.js +41 -222
- package/dist/src/utils/safeRegex.js.map +1 -1
- package/dist/src/utils/safeRegexApi.d.ts +56 -0
- package/dist/src/utils/safeRegexApi.js +99 -0
- package/dist/src/utils/safeRegexApi.js.map +1 -0
- package/dist/src/utils/safeRegexPool.d.ts +7 -0
- package/dist/src/utils/safeRegexPool.js +102 -0
- package/dist/src/utils/safeRegexPool.js.map +1 -0
- package/dist/src/utils/searchTools.js +6 -6
- package/dist/src/utils/searchTools.js.map +1 -1
- package/dist/src/utils/streaming.d.ts +4 -3
- package/dist/src/utils/streaming.js +1 -1
- package/dist/src/utils/streaming.js.map +1 -1
- package/dist/src/utils/telemetry.d.ts +1 -1
- package/dist/src/utils/telemetry.js +1 -1
- package/dist/src/utils/toctou.d.ts +7 -0
- package/dist/src/utils/toctou.js +24 -0
- package/dist/src/utils/toctou.js.map +1 -0
- package/llms.txt +173 -21
- package/package.json +2 -2
- package/scripts/replace-version.js +0 -59
package/dist/src/index.js
CHANGED
|
@@ -1,1021 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* mcp-code-context v3.
|
|
4
|
-
*
|
|
5
|
-
* Production-ready with:
|
|
6
|
-
* - Tree-sitter WASM for 100% AST accuracy (TypeScript, Python, PHP, Dart)
|
|
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
|
-
* - Persistent WASM SQLite cache with <100ms hits
|
|
12
|
-
* - Structured logging with pino (JSON to stderr, MCP-safe)
|
|
13
|
-
* - File watcher with chokidar for auto cache invalidation
|
|
14
|
-
* - Fuzzy search with fuse.js + pagination
|
|
15
|
-
* - Token optimization: compact diffs, auto-optimize output
|
|
3
|
+
* mcp-code-context v3.7.1 - Tree-sitter WASM Edition (thin bootstrap)
|
|
16
4
|
*/
|
|
17
5
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
-
import {
|
|
20
|
-
import * as path from "node:path";
|
|
21
|
-
import * as crypto from "node:crypto";
|
|
7
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
22
8
|
import { CodeContextEngine } from "./core/engine.js";
|
|
23
9
|
import { ParserRegistry } from "./parsers/registry.js";
|
|
24
|
-
import {
|
|
25
|
-
import { replaceSymbol, insertCode, removeSymbol, writeFile, renameSymbol } from "./operations/write.js";
|
|
26
|
-
import { extractSymbol, readLines, searchPattern, analyzeImpact, searchSymbols, explainSymbol, batchRead } from "./operations/read.js";
|
|
27
|
-
import { compressRepository } from "./operations/compress.js";
|
|
28
|
-
import { globalConfirmationStore } from "./operations/confirmationStore.js";
|
|
10
|
+
import { globalSessionManager } from "./core/sessionManager.js";
|
|
29
11
|
import { globalAuditLogger } from "./utils/auditLogger.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { streamFile } from "./utils/streaming.js";
|
|
34
|
-
import { BackupManager } from "./utils/backupManager.js";
|
|
35
|
-
import { CacheManager } from "./core/cacheManager.js";
|
|
36
|
-
import * as fs from "node:fs/promises";
|
|
12
|
+
import { TOOLS } from "./tools/toolDefinitions.js";
|
|
13
|
+
import { setServerInstances } from "./tools/context.js";
|
|
14
|
+
import { registerPipeline } from "./tools/pipeline.js";
|
|
37
15
|
const SERVER_NAME = "mcp-code-context";
|
|
38
|
-
const SERVER_VERSION = "3.
|
|
39
|
-
// Global instances
|
|
40
|
-
let engine;
|
|
41
|
-
let registry;
|
|
42
|
-
const rateLimiter = new RateLimiter();
|
|
43
|
-
// LRU cache for CacheManagers (max 5 projects to prevent FD exhaustion)
|
|
44
|
-
const cacheManagers = new Map();
|
|
45
|
-
const MAX_CACHE_MANAGERS = 5;
|
|
46
|
-
function getCacheManager(projectRoot) {
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
// Update last used time if exists
|
|
49
|
-
if (cacheManagers.has(projectRoot)) {
|
|
50
|
-
const entry = cacheManagers.get(projectRoot);
|
|
51
|
-
entry.lastUsed = now;
|
|
52
|
-
return entry.cache;
|
|
53
|
-
}
|
|
54
|
-
// Evict LRU if at capacity
|
|
55
|
-
if (cacheManagers.size >= MAX_CACHE_MANAGERS) {
|
|
56
|
-
let oldestKey = null;
|
|
57
|
-
let oldestTime = Infinity;
|
|
58
|
-
for (const [key, entry] of cacheManagers.entries()) {
|
|
59
|
-
if (entry.lastUsed < oldestTime) {
|
|
60
|
-
oldestTime = entry.lastUsed;
|
|
61
|
-
oldestKey = key;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (oldestKey) {
|
|
65
|
-
const evicted = cacheManagers.get(oldestKey);
|
|
66
|
-
evicted.cache.close(); // CRITICAL: Close watcher and free WASM heap
|
|
67
|
-
cacheManagers.delete(oldestKey);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// Create new cache manager
|
|
71
|
-
const cache = new CacheManager(projectRoot);
|
|
72
|
-
cacheManagers.set(projectRoot, { cache, lastUsed: now });
|
|
73
|
-
return cache;
|
|
74
|
-
}
|
|
75
|
-
// Write operations that require file locking and two-phase workflow
|
|
76
|
-
const WRITE_OPS = new Set(["replace_symbol", "insert_symbol", "remove_symbol", "rename_symbol"]);
|
|
77
|
-
const TOOLS = [
|
|
78
|
-
{
|
|
79
|
-
name: "get_semantic_repo_map",
|
|
80
|
-
description: "Generate a compressed architectural overview of an entire repository",
|
|
81
|
-
inputSchema: {
|
|
82
|
-
type: "object",
|
|
83
|
-
properties: {
|
|
84
|
-
directoryPath: { type: "string", description: "Absolute path to repository root" },
|
|
85
|
-
projectRoot: { type: "string", description: "Project root for security boundary" },
|
|
86
|
-
format: { type: "string", enum: ["xml", "markdown"], description: "Output format (default: xml)" },
|
|
87
|
-
},
|
|
88
|
-
required: ["directoryPath", "projectRoot"],
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: "read_file_surgical",
|
|
93
|
-
description: "Read a file or extract a specific named symbol",
|
|
94
|
-
inputSchema: {
|
|
95
|
-
type: "object",
|
|
96
|
-
properties: {
|
|
97
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
98
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
99
|
-
symbolName: { type: "string", description: "Symbol name to extract (optional)" },
|
|
100
|
-
className: { type: "string", description: "Class name for scoping (optional)" },
|
|
101
|
-
},
|
|
102
|
-
required: ["filePath", "projectRoot"],
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: "analyze_impact",
|
|
107
|
-
description: "Find all files that depend on a given file",
|
|
108
|
-
inputSchema: {
|
|
109
|
-
type: "object",
|
|
110
|
-
properties: {
|
|
111
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
112
|
-
projectRoot: { type: "string", description: "Project root for security boundary" },
|
|
113
|
-
rootDir: { type: "string", description: "Repository root (optional, defaults to projectRoot)" },
|
|
114
|
-
},
|
|
115
|
-
required: ["filePath", "projectRoot"],
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: "read_file_lines",
|
|
120
|
-
description: "Read specific line ranges from a file",
|
|
121
|
-
inputSchema: {
|
|
122
|
-
type: "object",
|
|
123
|
-
properties: {
|
|
124
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
125
|
-
projectRoot: { type: "string", description: "Project root for security boundary" },
|
|
126
|
-
startLine: { type: "number", description: "Starting line number (1-indexed)" },
|
|
127
|
-
endLine: { type: "number", description: "Ending line number (1-indexed)" },
|
|
128
|
-
aroundPattern: { type: "string", description: "Search pattern to find and return surrounding lines" },
|
|
129
|
-
contextLines: { type: "number", description: "Number of lines before/after pattern (default: 5)" },
|
|
130
|
-
},
|
|
131
|
-
required: ["filePath", "projectRoot"],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: "search_code_pattern",
|
|
136
|
-
description: "Search for code patterns across multiple files",
|
|
137
|
-
inputSchema: {
|
|
138
|
-
type: "object",
|
|
139
|
-
properties: {
|
|
140
|
-
rootDir: { type: "string", description: "Repository root directory" },
|
|
141
|
-
projectRoot: { type: "string", description: "Project root for security boundary" },
|
|
142
|
-
pattern: { type: "string", description: "Regular expression pattern to search" },
|
|
143
|
-
fileExtensions: { type: "array", items: { type: "string" }, description: "Extensions to search" },
|
|
144
|
-
excludeDirs: { type: "array", items: { type: "string" }, description: "Directories to exclude" },
|
|
145
|
-
maxResults: { type: "number", description: "Maximum matches to return (default: 10)" },
|
|
146
|
-
startIndex: { type: "number", description: "Start index for pagination (default: 0)" },
|
|
147
|
-
fuzzyMatch: { type: "boolean", description: "Enable fuzzy matching (default: false)" },
|
|
148
|
-
fuzzyThreshold: { type: "number", description: "Fuzzy match threshold 0-1 (default: 0.4)" },
|
|
149
|
-
},
|
|
150
|
-
required: ["rootDir", "projectRoot", "pattern"],
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
name: "parse_file",
|
|
155
|
-
description: "Parse a file using Tree-sitter and extract symbols",
|
|
156
|
-
inputSchema: {
|
|
157
|
-
type: "object",
|
|
158
|
-
properties: {
|
|
159
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
160
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
161
|
-
},
|
|
162
|
-
required: ["filePath", "projectRoot"],
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
name: "write_file_surgical",
|
|
167
|
-
description: "Replace a symbol with new code. Phase 1 (dry-run): returns diff + token. Phase 2: confirm with token to apply.",
|
|
168
|
-
inputSchema: {
|
|
169
|
-
type: "object",
|
|
170
|
-
properties: {
|
|
171
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
172
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
173
|
-
symbolName: { type: "string", description: "Symbol to replace" },
|
|
174
|
-
newContent: { type: "string", description: "New code (Phase 1 only — omit in Phase 2, server uses stored content)" },
|
|
175
|
-
className: { type: "string", description: "Class name (optional, for scoping)" },
|
|
176
|
-
confirm: { type: "boolean", description: "Set true to apply a pending operation (Phase 2)" },
|
|
177
|
-
confirmationToken: { type: "string", description: "Token from Phase 1 dry-run (Phase 2 only)" },
|
|
178
|
-
diffFormat: { type: "string", enum: ["unified", "compact", "summary", "none"], description: "Diff verbosity in Phase 1 output (default: unified). Use none to skip diff and save tokens." },
|
|
179
|
-
},
|
|
180
|
-
required: ["filePath", "projectRoot", "symbolName"],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: "insert_symbol",
|
|
185
|
-
description: "Insert code at a specific location. Phase 1: returns diff + token. Phase 2: confirm with token to apply.",
|
|
186
|
-
inputSchema: {
|
|
187
|
-
type: "object",
|
|
188
|
-
properties: {
|
|
189
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
190
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
191
|
-
code: { type: "string", description: "Code to insert (Phase 1 only — omit in Phase 2, server uses stored content)" },
|
|
192
|
-
anchorSymbol: { type: "string", description: "Symbol to position relative to" },
|
|
193
|
-
position: { type: "string", enum: ["before", "after", "inside_start", "inside_end"], description: "Where to insert" },
|
|
194
|
-
className: { type: "string", description: "Class name (optional)" },
|
|
195
|
-
confirm: { type: "boolean", description: "Set true to apply a pending operation (Phase 2)" },
|
|
196
|
-
confirmationToken: { type: "string", description: "Token from Phase 1 dry-run (Phase 2 only)" },
|
|
197
|
-
diffFormat: { type: "string", enum: ["unified", "compact", "summary", "none"], description: "Diff verbosity in Phase 1 output (default: unified). Use none to skip diff and save tokens." },
|
|
198
|
-
},
|
|
199
|
-
required: ["filePath", "projectRoot"],
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
name: "remove_symbol",
|
|
204
|
-
description: "Remove a symbol from file. Phase 1: returns diff + token. Phase 2: confirm with token to apply.",
|
|
205
|
-
inputSchema: {
|
|
206
|
-
type: "object",
|
|
207
|
-
properties: {
|
|
208
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
209
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
210
|
-
symbolName: { type: "string", description: "Symbol to remove" },
|
|
211
|
-
className: { type: "string", description: "Class name (optional)" },
|
|
212
|
-
force: { type: "boolean", description: "Skip dependency check" },
|
|
213
|
-
confirm: { type: "boolean", description: "Set true to apply a pending operation" },
|
|
214
|
-
confirmationToken: { type: "string", description: "Token from Phase 1 dry-run" },
|
|
215
|
-
},
|
|
216
|
-
required: ["filePath", "projectRoot", "symbolName"],
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
name: "rename_symbol",
|
|
221
|
-
description: "Rename a symbol across the entire repository. Phase 1: returns diff + token. Phase 2: confirm with token to apply.",
|
|
222
|
-
inputSchema: {
|
|
223
|
-
type: "object",
|
|
224
|
-
properties: {
|
|
225
|
-
filePath: { type: "string", description: "File where symbol is defined" },
|
|
226
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
227
|
-
oldName: { type: "string", description: "Current name" },
|
|
228
|
-
newName: { type: "string", description: "New name" },
|
|
229
|
-
rootDir: { type: "string", description: "Repository root (optional)" },
|
|
230
|
-
confirm: { type: "boolean", description: "Set true to apply a pending operation" },
|
|
231
|
-
confirmationToken: { type: "string", description: "Token from Phase 1 dry-run" },
|
|
232
|
-
},
|
|
233
|
-
required: ["filePath", "projectRoot", "oldName", "newName"],
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
// ── NEW TOOLS v3.6.3 ──────────────────────────────────────────────────────
|
|
237
|
-
{
|
|
238
|
-
name: "search_symbols",
|
|
239
|
-
description: "Search symbols by name across the repo using AST (not text search). Finds classes, functions, methods by approximate name.",
|
|
240
|
-
inputSchema: {
|
|
241
|
-
type: "object",
|
|
242
|
-
properties: {
|
|
243
|
-
rootDir: { type: "string", description: "Directory to search in" },
|
|
244
|
-
projectRoot: { type: "string", description: "Project root for security boundary" },
|
|
245
|
-
query: { type: "string", description: "Symbol name or partial name to search" },
|
|
246
|
-
fuzzy: { type: "boolean", description: "Enable fuzzy name matching (default: false)" },
|
|
247
|
-
types: { type: "array", items: { type: "string" }, description: "Filter by symbol types: class_declaration, function_declaration, method_definition, etc." },
|
|
248
|
-
fileExtensions: { type: "array", items: { type: "string" }, description: "Extensions to search (default: all supported)" },
|
|
249
|
-
excludeDirs: { type: "array", items: { type: "string" }, description: "Directories to exclude" },
|
|
250
|
-
maxResults: { type: "number", description: "Maximum results (default: 20)" },
|
|
251
|
-
},
|
|
252
|
-
required: ["rootDir", "projectRoot", "query"],
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
name: "explain_symbol",
|
|
257
|
-
description: "Get signature, location, and callers of a symbol in one call. More efficient than read_file_surgical + analyze_impact separately.",
|
|
258
|
-
inputSchema: {
|
|
259
|
-
type: "object",
|
|
260
|
-
properties: {
|
|
261
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
262
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
263
|
-
symbolName: { type: "string", description: "Symbol to explain" },
|
|
264
|
-
className: { type: "string", description: "Class name (optional, for scoping)" },
|
|
265
|
-
rootDir: { type: "string", description: "Root dir for caller search (optional, defaults to projectRoot)" },
|
|
266
|
-
},
|
|
267
|
-
required: ["filePath", "projectRoot", "symbolName"],
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: "batch_read",
|
|
272
|
-
description: "Read multiple symbols from multiple files in one call. Reduces N round-trips to 1.",
|
|
273
|
-
inputSchema: {
|
|
274
|
-
type: "object",
|
|
275
|
-
properties: {
|
|
276
|
-
reads: {
|
|
277
|
-
type: "array",
|
|
278
|
-
description: "List of symbols to read",
|
|
279
|
-
items: {
|
|
280
|
-
type: "object",
|
|
281
|
-
properties: {
|
|
282
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
283
|
-
symbolName: { type: "string", description: "Symbol name to extract" },
|
|
284
|
-
className: { type: "string", description: "Class name (optional)" },
|
|
285
|
-
},
|
|
286
|
-
required: ["filePath", "symbolName"],
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
projectRoot: { type: "string", description: "Project root for security boundary" },
|
|
290
|
-
},
|
|
291
|
-
required: ["reads", "projectRoot"],
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "get_rate_limit_status",
|
|
296
|
-
description: "Get current rate limiter token balance and operation costs. Use before expensive operations to check available budget.",
|
|
297
|
-
inputSchema: {
|
|
298
|
-
type: "object",
|
|
299
|
-
properties: {},
|
|
300
|
-
required: [],
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
// ── END NEW TOOLS ──────────────────────────────────────────────────────────
|
|
304
|
-
{
|
|
305
|
-
name: "rollback_file",
|
|
306
|
-
description: "Revert a file to its backup state",
|
|
307
|
-
inputSchema: {
|
|
308
|
-
type: "object",
|
|
309
|
-
properties: {
|
|
310
|
-
filePath: { type: "string", description: "Absolute path to file" },
|
|
311
|
-
projectRoot: { type: "string", description: "Project root (REQUIRED)" },
|
|
312
|
-
},
|
|
313
|
-
required: ["filePath", "projectRoot"],
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
name: "clean_backups",
|
|
318
|
-
description: "Remove all backup files for a project",
|
|
319
|
-
inputSchema: {
|
|
320
|
-
type: "object",
|
|
321
|
-
properties: {
|
|
322
|
-
projectRoot: { type: "string", description: "Project root directory" },
|
|
323
|
-
},
|
|
324
|
-
required: ["projectRoot"],
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
name: "get_server_stats",
|
|
329
|
-
description: "Get server telemetry, audit statistics, and health metrics",
|
|
330
|
-
inputSchema: {
|
|
331
|
-
type: "object",
|
|
332
|
-
properties: {},
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
name: "get_cache_stats",
|
|
337
|
-
description: "Get cache statistics (entries, size, hit rate)",
|
|
338
|
-
inputSchema: {
|
|
339
|
-
type: "object",
|
|
340
|
-
properties: {
|
|
341
|
-
projectRoot: { type: "string", description: "Project root directory" },
|
|
342
|
-
},
|
|
343
|
-
required: ["projectRoot"],
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
name: "clear_cache",
|
|
348
|
-
description: "Clear all cached parse results for a project",
|
|
349
|
-
inputSchema: {
|
|
350
|
-
type: "object",
|
|
351
|
-
properties: {
|
|
352
|
-
projectRoot: { type: "string", description: "Project root directory" },
|
|
353
|
-
},
|
|
354
|
-
required: ["projectRoot"],
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
name: "configure_file_watcher",
|
|
359
|
-
description: "Start/stop file watcher for auto-cache invalidation",
|
|
360
|
-
inputSchema: {
|
|
361
|
-
type: "object",
|
|
362
|
-
properties: {
|
|
363
|
-
projectRoot: { type: "string", description: "Project root directory" },
|
|
364
|
-
action: { type: "string", enum: ["start", "stop"], description: "Action to perform" },
|
|
365
|
-
debounceMs: { type: "number", description: "Debounce delay in ms (default: 500)" },
|
|
366
|
-
},
|
|
367
|
-
required: ["projectRoot", "action"],
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: "get_file_watcher_status",
|
|
372
|
-
description: "Get file watcher status for a project",
|
|
373
|
-
inputSchema: {
|
|
374
|
-
type: "object",
|
|
375
|
-
properties: {
|
|
376
|
-
projectRoot: { type: "string", description: "Project root directory" },
|
|
377
|
-
},
|
|
378
|
-
required: ["projectRoot"],
|
|
379
|
-
},
|
|
380
|
-
},
|
|
381
|
-
];
|
|
16
|
+
const SERVER_VERSION = "3.7.1";
|
|
382
17
|
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
383
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
384
|
-
|
|
385
|
-
}));
|
|
386
|
-
// Setup connection and initialize engine
|
|
18
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
19
|
+
registerPipeline(server);
|
|
387
20
|
async function main() {
|
|
388
|
-
engine = new CodeContextEngine();
|
|
21
|
+
const engine = new CodeContextEngine();
|
|
389
22
|
await engine.init();
|
|
390
|
-
registry = new ParserRegistry(engine);
|
|
23
|
+
const registry = new ParserRegistry(engine);
|
|
391
24
|
await registry.init();
|
|
25
|
+
setServerInstances(engine, registry);
|
|
392
26
|
const transport = new StdioServerTransport();
|
|
393
27
|
await server.connect(transport);
|
|
394
28
|
console.error(`[${SERVER_NAME}] v${SERVER_VERSION} running on stdio`);
|
|
395
29
|
}
|
|
396
|
-
// -----------------------------------------------------------------------------
|
|
397
|
-
// READ HANDLERS
|
|
398
|
-
// -----------------------------------------------------------------------------
|
|
399
|
-
async function handleGetSemanticRepoMap(args) {
|
|
400
|
-
const directoryPath = String(args.directoryPath);
|
|
401
|
-
const projectRoot = String(args.projectRoot);
|
|
402
|
-
const format = args.format === "markdown" ? "markdown" : "xml";
|
|
403
|
-
const validator = new SecurityValidator(projectRoot);
|
|
404
|
-
const validation = await validator.validateFilePath(directoryPath);
|
|
405
|
-
if (!validation.valid) {
|
|
406
|
-
throw new Error(validation.error);
|
|
407
|
-
}
|
|
408
|
-
const result = await compressRepository({
|
|
409
|
-
directoryPath: validation.resolvedPath,
|
|
410
|
-
registry,
|
|
411
|
-
format: format
|
|
412
|
-
});
|
|
413
|
-
if (!result.success)
|
|
414
|
-
throw new Error(result.error);
|
|
415
|
-
return {
|
|
416
|
-
content: [{ type: "text", text: result.content }],
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
async function handleReadFileSurgical(args) {
|
|
420
|
-
const filePath = String(args.filePath);
|
|
421
|
-
const projectRoot = String(args.projectRoot);
|
|
422
|
-
const symbolName = args.symbolName ? String(args.symbolName) : undefined;
|
|
423
|
-
const className = args.className ? String(args.className) : undefined;
|
|
424
|
-
const validator = new SecurityValidator(projectRoot);
|
|
425
|
-
const validation = await validator.validateFilePath(filePath);
|
|
426
|
-
if (!validation.valid)
|
|
427
|
-
throw new Error(validation.error);
|
|
428
|
-
let content;
|
|
429
|
-
if (symbolName) {
|
|
430
|
-
const ext = path.extname(filePath);
|
|
431
|
-
const parser = registry.getParser(ext);
|
|
432
|
-
if (!parser)
|
|
433
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
434
|
-
const result = await extractSymbol({
|
|
435
|
-
filePath: validation.resolvedPath,
|
|
436
|
-
projectRoot,
|
|
437
|
-
symbolName,
|
|
438
|
-
className,
|
|
439
|
-
parser,
|
|
440
|
-
cache: getCacheManager(projectRoot)
|
|
441
|
-
});
|
|
442
|
-
if (!result.success)
|
|
443
|
-
throw new Error(result.error);
|
|
444
|
-
content = result.content;
|
|
445
|
-
}
|
|
446
|
-
else {
|
|
447
|
-
// If > 5MB, stream it to avoid memory issues
|
|
448
|
-
const stat = await fs.stat(validation.resolvedPath);
|
|
449
|
-
if (stat.size > 5 * 1024 * 1024) {
|
|
450
|
-
const streamRes = await streamFile(validation.resolvedPath);
|
|
451
|
-
if (!streamRes.success)
|
|
452
|
-
throw new Error(streamRes.error);
|
|
453
|
-
content = streamRes.chunks.join("");
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
content = await fs.readFile(validation.resolvedPath, "utf-8");
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return { content: [{ type: "text", text: content }] };
|
|
460
|
-
}
|
|
461
|
-
async function handleAnalyzeImpact(args) {
|
|
462
|
-
const filePath = String(args.filePath);
|
|
463
|
-
const projectRoot = String(args.projectRoot);
|
|
464
|
-
const rootDir = args.rootDir ? String(args.rootDir) : projectRoot;
|
|
465
|
-
const validator = new SecurityValidator(projectRoot);
|
|
466
|
-
const validation = await validator.validateFilePath(filePath);
|
|
467
|
-
if (!validation.valid)
|
|
468
|
-
throw new Error(validation.error);
|
|
469
|
-
const rootValidation = await validator.validateFilePath(rootDir);
|
|
470
|
-
if (!rootValidation.valid)
|
|
471
|
-
throw new Error(rootValidation.error);
|
|
472
|
-
const result = await analyzeImpact({ filePath: validation.resolvedPath, rootDir: rootValidation.resolvedPath });
|
|
473
|
-
if (!result.success)
|
|
474
|
-
throw new Error(result.error);
|
|
475
|
-
return { content: [{ type: "text", text: result.content }] };
|
|
476
|
-
}
|
|
477
|
-
async function handleReadFileLines(args) {
|
|
478
|
-
const filePath = String(args.filePath);
|
|
479
|
-
const projectRoot = String(args.projectRoot);
|
|
480
|
-
const validator = new SecurityValidator(projectRoot);
|
|
481
|
-
const validation = await validator.validateFilePath(filePath);
|
|
482
|
-
if (!validation.valid)
|
|
483
|
-
throw new Error(validation.error);
|
|
484
|
-
const result = await readLines({
|
|
485
|
-
filePath: validation.resolvedPath,
|
|
486
|
-
startLine: args.startLine,
|
|
487
|
-
endLine: args.endLine,
|
|
488
|
-
aroundPattern: args.aroundPattern,
|
|
489
|
-
contextLines: args.contextLines,
|
|
490
|
-
});
|
|
491
|
-
if (!result.success)
|
|
492
|
-
throw new Error(result.error);
|
|
493
|
-
return { content: [{ type: "text", text: result.content }] };
|
|
494
|
-
}
|
|
495
|
-
async function handleSearchCodePattern(args) {
|
|
496
|
-
const rootDir = String(args.rootDir);
|
|
497
|
-
const projectRoot = String(args.projectRoot);
|
|
498
|
-
const validator = new SecurityValidator(projectRoot);
|
|
499
|
-
const validation = await validator.validateFilePath(rootDir);
|
|
500
|
-
if (!validation.valid)
|
|
501
|
-
throw new Error(validation.error);
|
|
502
|
-
const result = await searchPattern({
|
|
503
|
-
rootDir: validation.resolvedPath,
|
|
504
|
-
pattern: String(args.pattern),
|
|
505
|
-
fileExtensions: args.fileExtensions,
|
|
506
|
-
excludeDirs: args.excludeDirs,
|
|
507
|
-
maxResults: args.maxResults,
|
|
508
|
-
startIndex: args.startIndex,
|
|
509
|
-
fuzzyMatch: args.fuzzyMatch,
|
|
510
|
-
fuzzyThreshold: args.fuzzyThreshold,
|
|
511
|
-
});
|
|
512
|
-
if (!result.success)
|
|
513
|
-
throw new Error(result.error);
|
|
514
|
-
return { content: [{ type: "text", text: result.content }] };
|
|
515
|
-
}
|
|
516
|
-
async function handleParseFile(args) {
|
|
517
|
-
const filePath = String(args.filePath);
|
|
518
|
-
const projectRoot = String(args.projectRoot);
|
|
519
|
-
const validator = new SecurityValidator(projectRoot);
|
|
520
|
-
const validation = await validator.validateFilePath(filePath);
|
|
521
|
-
if (!validation.valid)
|
|
522
|
-
throw new Error(validation.error);
|
|
523
|
-
const content = await fs.readFile(validation.resolvedPath, "utf-8");
|
|
524
|
-
const ext = path.extname(validation.resolvedPath);
|
|
525
|
-
const parser = registry.getParser(ext);
|
|
526
|
-
if (!parser)
|
|
527
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
528
|
-
const tree = parser.parse(content);
|
|
529
|
-
const symbols = parser.findSymbols(tree);
|
|
530
|
-
return {
|
|
531
|
-
content: [{ type: "text", text: JSON.stringify(symbols, null, 2) }],
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
// -----------------------------------------------------------------------------
|
|
535
|
-
// WRITE HANDLERS (Two-Phase)
|
|
536
|
-
// -----------------------------------------------------------------------------
|
|
537
|
-
async function handleTwoPhaseWrite(args, operationName, executeWrite) {
|
|
538
|
-
const confirm = Boolean(args.confirm);
|
|
539
|
-
const token = args.confirmationToken ? String(args.confirmationToken) : undefined;
|
|
540
|
-
if (confirm) {
|
|
541
|
-
if (!token)
|
|
542
|
-
throw new Error("confirmationToken is required when confirm=true");
|
|
543
|
-
const pendingOp = globalConfirmationStore.consumePending(token);
|
|
544
|
-
if (!pendingOp)
|
|
545
|
-
throw new Error(`Invalid or expired confirmation token: ${token}`);
|
|
546
|
-
const projectRoot = String(args.projectRoot);
|
|
547
|
-
const validator = new SecurityValidator(projectRoot);
|
|
548
|
-
const lockReleases = [];
|
|
549
|
-
try {
|
|
550
|
-
// TOCTOU CHECK: Verify file hasn't been modified since Phase 1
|
|
551
|
-
if (pendingOp.originalHash) {
|
|
552
|
-
const validation = await validator.validateFilePath(pendingOp.filePath);
|
|
553
|
-
if (!validation.valid)
|
|
554
|
-
throw new Error(validation.error);
|
|
555
|
-
const currentContent = await fs.readFile(validation.resolvedPath, "utf-8");
|
|
556
|
-
const currentHash = crypto.createHash('md5').update(currentContent).digest('hex');
|
|
557
|
-
if (currentHash !== pendingOp.originalHash) {
|
|
558
|
-
throw new Error("File was modified by another process after Phase 1. Please repeat the operation to prevent data loss.");
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
if (pendingOp.pendingWrites && pendingOp.pendingWrites.length > 0) {
|
|
562
|
-
const sortedWrites = [...pendingOp.pendingWrites].sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
563
|
-
for (const pw of sortedWrites) {
|
|
564
|
-
const validation = await validator.validateFilePath(pw.filePath);
|
|
565
|
-
if (!validation.valid)
|
|
566
|
-
throw new Error(validation.error);
|
|
567
|
-
if (!globalLockManager.isLocked(validation.resolvedPath)) {
|
|
568
|
-
const release = await globalLockManager.acquireLock(validation.resolvedPath);
|
|
569
|
-
lockReleases.push(release);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
for (const pw of sortedWrites) {
|
|
573
|
-
const validation = await validator.validateFilePath(pw.filePath);
|
|
574
|
-
await BackupManager.createBackup(validation.resolvedPath, projectRoot);
|
|
575
|
-
await writeFile(validation.resolvedPath, pw.newContent);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
else {
|
|
579
|
-
const validation = await validator.validateFilePath(pendingOp.filePath);
|
|
580
|
-
if (!validation.valid)
|
|
581
|
-
throw new Error(validation.error);
|
|
582
|
-
if (!globalLockManager.isLocked(validation.resolvedPath)) {
|
|
583
|
-
const release = await globalLockManager.acquireLock(validation.resolvedPath);
|
|
584
|
-
lockReleases.push(release);
|
|
585
|
-
}
|
|
586
|
-
await BackupManager.createBackup(validation.resolvedPath, projectRoot);
|
|
587
|
-
await writeFile(validation.resolvedPath, pendingOp.newContent);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
finally {
|
|
591
|
-
for (const release of lockReleases) {
|
|
592
|
-
await release();
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
const fileCount = pendingOp.pendingWrites?.length || 1;
|
|
596
|
-
return {
|
|
597
|
-
content: [{ type: "text", text: `✅ Success. Changes applied to ${fileCount} file(s).` }],
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
// Phase 1: Dry-run
|
|
601
|
-
const result = await executeWrite();
|
|
602
|
-
if (!result.success)
|
|
603
|
-
throw new Error(result.error);
|
|
604
|
-
const hadConflict = globalConfirmationStore.hasConflictingPending(String(args.filePath));
|
|
605
|
-
const newToken = globalConfirmationStore.storePending({
|
|
606
|
-
filePath: String(args.filePath),
|
|
607
|
-
operation: operationName,
|
|
608
|
-
symbolName: args.symbolName ? String(args.symbolName) : undefined,
|
|
609
|
-
newContent: result.newContent,
|
|
610
|
-
diff: result.diff,
|
|
611
|
-
originalHash: result.originalHash,
|
|
612
|
-
pendingWrites: result.pendingWrites,
|
|
613
|
-
});
|
|
614
|
-
const conflictWarning = hadConflict
|
|
615
|
-
? `\n⚠️ A previous pending token for this file was invalidated. Only this new token is valid.`
|
|
616
|
-
: "";
|
|
617
|
-
// ERG-01 + NEW-04: decode diff and apply diffFormat
|
|
618
|
-
const rawDiff = decodeURIComponent(result.diff);
|
|
619
|
-
const fmt = args.diffFormat ? String(args.diffFormat) : "unified";
|
|
620
|
-
let displayDiff;
|
|
621
|
-
if (fmt === "none") {
|
|
622
|
-
displayDiff = `(diff omitted — use diffFormat="unified" to see changes)`;
|
|
623
|
-
}
|
|
624
|
-
else if (fmt === "summary") {
|
|
625
|
-
const added = (rawDiff.match(/^\+[^+]/gm) || []).length;
|
|
626
|
-
const removed = (rawDiff.match(/^-[^-]/gm) || []).length;
|
|
627
|
-
displayDiff = `+${added} lines, -${removed} lines`;
|
|
628
|
-
}
|
|
629
|
-
else if (fmt === "compact") {
|
|
630
|
-
displayDiff = rawDiff.split("\n").filter(l => l.startsWith("+") || l.startsWith("-")).join("\n");
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
displayDiff = rawDiff;
|
|
634
|
-
}
|
|
635
|
-
return {
|
|
636
|
-
content: [{
|
|
637
|
-
type: "text",
|
|
638
|
-
text: `DRY RUN SUCCESSFUL.${conflictWarning}\nTo apply these changes, call this tool again with confirm=true and confirmationToken="${newToken}"\n\nDiff:\n${displayDiff}`
|
|
639
|
-
}],
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
async function handleWriteFileSurgical(args) {
|
|
643
|
-
return handleTwoPhaseWrite(args, "write_file_surgical", async () => {
|
|
644
|
-
const ext = path.extname(String(args.filePath));
|
|
645
|
-
const parser = registry.getParser(ext);
|
|
646
|
-
if (!parser)
|
|
647
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
648
|
-
return await replaceSymbol({
|
|
649
|
-
filePath: String(args.filePath),
|
|
650
|
-
projectRoot: String(args.projectRoot),
|
|
651
|
-
symbolName: String(args.symbolName),
|
|
652
|
-
newContent: String(args.newContent),
|
|
653
|
-
className: args.className ? String(args.className) : undefined,
|
|
654
|
-
parser,
|
|
655
|
-
});
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
async function handleInsertSymbol(args) {
|
|
659
|
-
return handleTwoPhaseWrite(args, "insert_symbol", async () => {
|
|
660
|
-
const ext = path.extname(String(args.filePath));
|
|
661
|
-
const parser = registry.getParser(ext);
|
|
662
|
-
if (!parser)
|
|
663
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
664
|
-
return await insertCode({
|
|
665
|
-
filePath: String(args.filePath),
|
|
666
|
-
projectRoot: String(args.projectRoot),
|
|
667
|
-
code: String(args.code),
|
|
668
|
-
anchorSymbol: args.anchorSymbol ? String(args.anchorSymbol) : undefined,
|
|
669
|
-
position: args.position,
|
|
670
|
-
className: args.className ? String(args.className) : undefined,
|
|
671
|
-
parser,
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
async function handleRemoveSymbol(args) {
|
|
676
|
-
return handleTwoPhaseWrite(args, "remove_symbol", async () => {
|
|
677
|
-
const ext = path.extname(String(args.filePath));
|
|
678
|
-
const parser = registry.getParser(ext);
|
|
679
|
-
if (!parser)
|
|
680
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
681
|
-
return await removeSymbol({
|
|
682
|
-
filePath: String(args.filePath),
|
|
683
|
-
projectRoot: String(args.projectRoot),
|
|
684
|
-
symbolName: String(args.symbolName),
|
|
685
|
-
className: args.className ? String(args.className) : undefined,
|
|
686
|
-
parser,
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
async function handleRenameSymbol(args) {
|
|
691
|
-
return handleTwoPhaseWrite(args, "rename_symbol", async () => {
|
|
692
|
-
const ext = path.extname(String(args.filePath));
|
|
693
|
-
const parser = registry.getParser(ext);
|
|
694
|
-
if (!parser)
|
|
695
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
696
|
-
return await renameSymbol({
|
|
697
|
-
filePath: String(args.filePath),
|
|
698
|
-
projectRoot: String(args.projectRoot),
|
|
699
|
-
oldName: String(args.oldName),
|
|
700
|
-
newName: String(args.newName),
|
|
701
|
-
rootDir: args.rootDir ? String(args.rootDir) : String(args.projectRoot),
|
|
702
|
-
parser,
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
// -----------------------------------------------------------------------------
|
|
707
|
-
// OTHER HANDLERS
|
|
708
|
-
// -----------------------------------------------------------------------------
|
|
709
|
-
async function handleRollbackFile(args) {
|
|
710
|
-
const filePath = String(args.filePath);
|
|
711
|
-
const projectRoot = String(args.projectRoot);
|
|
712
|
-
const steps = args.steps ? Number(args.steps) : 1;
|
|
713
|
-
const validator = new SecurityValidator(projectRoot);
|
|
714
|
-
const validation = await validator.validateFilePath(filePath);
|
|
715
|
-
if (!validation.valid)
|
|
716
|
-
throw new Error(validation.error);
|
|
717
|
-
const result = await BackupManager.rollback(validation.resolvedPath, projectRoot, steps);
|
|
718
|
-
if (!result.success)
|
|
719
|
-
throw new Error(result.error);
|
|
720
|
-
return { content: [{ type: "text", text: `Successfully rolled back ${path.basename(filePath)} from backup: ${result.restoredFrom}` }] };
|
|
721
|
-
}
|
|
722
|
-
async function handleCleanBackups(args) {
|
|
723
|
-
const projectRoot = String(args.projectRoot);
|
|
724
|
-
const validator = new SecurityValidator(projectRoot);
|
|
725
|
-
// Just validate projectRoot is a valid directory
|
|
726
|
-
const validation = await validator.validateFilePath(projectRoot);
|
|
727
|
-
if (!validation.valid)
|
|
728
|
-
throw new Error(validation.error);
|
|
729
|
-
const result = await BackupManager.clean(projectRoot);
|
|
730
|
-
if (!result.success)
|
|
731
|
-
throw new Error(result.error);
|
|
732
|
-
return { content: [{ type: "text", text: `Successfully cleaned backups. Deleted ${result.deletedCount} backup files.` }] };
|
|
733
|
-
}
|
|
734
|
-
async function handleGetServerStats() {
|
|
735
|
-
const telemetry = globalTelemetry.getSummary();
|
|
736
|
-
const audit = globalAuditLogger.getStats();
|
|
737
|
-
const tokensAvailable = rateLimiter.getTokens("global");
|
|
738
|
-
const stats = {
|
|
739
|
-
pendingConfirmations: globalConfirmationStore.getPendingCount(),
|
|
740
|
-
rateLimiter: {
|
|
741
|
-
tokensAvailable,
|
|
742
|
-
maxTokens: 100,
|
|
743
|
-
refillRate: "10 tokens/second",
|
|
744
|
-
operationCosts: OPERATION_COSTS,
|
|
745
|
-
},
|
|
746
|
-
telemetry,
|
|
747
|
-
audit,
|
|
748
|
-
};
|
|
749
|
-
return {
|
|
750
|
-
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
async function handleGetCacheStats(args) {
|
|
754
|
-
const projectRoot = String(args.projectRoot);
|
|
755
|
-
const cache = getCacheManager(projectRoot);
|
|
756
|
-
const stats = await cache.getStats();
|
|
757
|
-
return {
|
|
758
|
-
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }],
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
async function handleClearCache(args) {
|
|
762
|
-
const projectRoot = String(args.projectRoot);
|
|
763
|
-
const cache = getCacheManager(projectRoot);
|
|
764
|
-
await cache.clear();
|
|
765
|
-
return {
|
|
766
|
-
content: [{ type: "text", text: "Cache cleared successfully" }],
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
async function handleConfigureFileWatcher(args) {
|
|
770
|
-
const projectRoot = String(args.projectRoot);
|
|
771
|
-
const action = String(args.action);
|
|
772
|
-
const debounceMs = args.debounceMs ? Number(args.debounceMs) : 500;
|
|
773
|
-
const cache = getCacheManager(projectRoot);
|
|
774
|
-
if (action === "start") {
|
|
775
|
-
cache.startWatcher(debounceMs);
|
|
776
|
-
return {
|
|
777
|
-
content: [{ type: "text", text: `File watcher started with ${debounceMs}ms debounce` }],
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
else if (action === "stop") {
|
|
781
|
-
cache.stopWatcher();
|
|
782
|
-
return {
|
|
783
|
-
content: [{ type: "text", text: "File watcher stopped" }],
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
throw new Error(`Invalid action: ${action}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
async function handleGetFileWatcherStatus(args) {
|
|
791
|
-
const projectRoot = String(args.projectRoot);
|
|
792
|
-
const cache = getCacheManager(projectRoot);
|
|
793
|
-
const status = cache.getWatcherStatus();
|
|
794
|
-
return {
|
|
795
|
-
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
// NEW-01: get_rate_limit_status
|
|
799
|
-
async function handleGetRateLimitStatus() {
|
|
800
|
-
const tokensAvailable = rateLimiter.getTokens("global");
|
|
801
|
-
const result = {
|
|
802
|
-
tokensAvailable,
|
|
803
|
-
maxTokens: 100,
|
|
804
|
-
refillRate: "10 tokens/second",
|
|
805
|
-
canAfford: Object.fromEntries(Object.entries(OPERATION_COSTS).map(([op, cost]) => [op, tokensAvailable >= cost])),
|
|
806
|
-
operationCosts: OPERATION_COSTS,
|
|
807
|
-
};
|
|
808
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
809
|
-
}
|
|
810
|
-
// NEW-02: search_symbols
|
|
811
|
-
async function handleSearchSymbols(args) {
|
|
812
|
-
const rootDir = String(args.rootDir);
|
|
813
|
-
const projectRoot = String(args.projectRoot);
|
|
814
|
-
const validator = new SecurityValidator(projectRoot);
|
|
815
|
-
const validation = await validator.validateFilePath(rootDir);
|
|
816
|
-
if (!validation.valid)
|
|
817
|
-
throw new Error(validation.error);
|
|
818
|
-
const result = await searchSymbols({
|
|
819
|
-
rootDir: validation.resolvedPath,
|
|
820
|
-
projectRoot,
|
|
821
|
-
query: String(args.query),
|
|
822
|
-
fuzzy: args.fuzzy,
|
|
823
|
-
types: args.types,
|
|
824
|
-
fileExtensions: args.fileExtensions,
|
|
825
|
-
excludeDirs: args.excludeDirs,
|
|
826
|
-
maxResults: args.maxResults,
|
|
827
|
-
});
|
|
828
|
-
if (!result.success)
|
|
829
|
-
throw new Error(result.error);
|
|
830
|
-
return { content: [{ type: "text", text: result.content }] };
|
|
831
|
-
}
|
|
832
|
-
// NEW-03: explain_symbol
|
|
833
|
-
async function handleExplainSymbol(args) {
|
|
834
|
-
const filePath = String(args.filePath);
|
|
835
|
-
const projectRoot = String(args.projectRoot);
|
|
836
|
-
const rootDir = args.rootDir ? String(args.rootDir) : projectRoot;
|
|
837
|
-
const validator = new SecurityValidator(projectRoot);
|
|
838
|
-
const validation = await validator.validateFilePath(filePath);
|
|
839
|
-
if (!validation.valid)
|
|
840
|
-
throw new Error(validation.error);
|
|
841
|
-
const ext = path.extname(validation.resolvedPath);
|
|
842
|
-
const parser = registry.getParser(ext);
|
|
843
|
-
if (!parser)
|
|
844
|
-
throw new Error(`No parser available for ${ext} files`);
|
|
845
|
-
const result = await explainSymbol({
|
|
846
|
-
filePath: validation.resolvedPath,
|
|
847
|
-
projectRoot,
|
|
848
|
-
symbolName: String(args.symbolName),
|
|
849
|
-
className: args.className ? String(args.className) : undefined,
|
|
850
|
-
parser,
|
|
851
|
-
rootDir,
|
|
852
|
-
});
|
|
853
|
-
if (!result.success)
|
|
854
|
-
throw new Error(result.error);
|
|
855
|
-
return { content: [{ type: "text", text: result.content }] };
|
|
856
|
-
}
|
|
857
|
-
// NEW-05: batch_read
|
|
858
|
-
async function handleBatchRead(args) {
|
|
859
|
-
const projectRoot = String(args.projectRoot);
|
|
860
|
-
const reads = args.reads;
|
|
861
|
-
const validator = new SecurityValidator(projectRoot);
|
|
862
|
-
// Validate all paths upfront
|
|
863
|
-
for (const read of reads) {
|
|
864
|
-
const v = await validator.validateFilePath(read.filePath);
|
|
865
|
-
if (!v.valid)
|
|
866
|
-
throw new Error(v.error);
|
|
867
|
-
read.filePath = v.resolvedPath;
|
|
868
|
-
}
|
|
869
|
-
const result = await batchRead({
|
|
870
|
-
reads,
|
|
871
|
-
projectRoot,
|
|
872
|
-
parser: (ext) => registry.getParser(ext) ?? null,
|
|
873
|
-
});
|
|
874
|
-
if (!result.success)
|
|
875
|
-
throw new Error(result.error);
|
|
876
|
-
return { content: [{ type: "text", text: result.content }] };
|
|
877
|
-
}
|
|
878
|
-
// -----------------------------------------------------------------------------
|
|
879
|
-
// PIPELINE & MAIN EXECUTION
|
|
880
|
-
// -----------------------------------------------------------------------------
|
|
881
|
-
/**
|
|
882
|
-
* Redact sensitive fields from args before audit logging.
|
|
883
|
-
* Prevents newContent/code from being persisted in plain text.
|
|
884
|
-
*/
|
|
885
|
-
function redactSensitiveFields(args) {
|
|
886
|
-
const SENSITIVE_KEYS = ["newContent", "code", "content"];
|
|
887
|
-
const safe = { ...args };
|
|
888
|
-
for (const key of SENSITIVE_KEYS) {
|
|
889
|
-
if (key in safe && typeof safe[key] === "string") {
|
|
890
|
-
safe[key] = `[REDACTED: ${safe[key].length} chars]`;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return safe;
|
|
894
|
-
}
|
|
895
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
896
|
-
const { name, arguments: args } = request.params;
|
|
897
|
-
const startTime = Date.now();
|
|
898
|
-
let success = false;
|
|
899
|
-
let lockReleased = false;
|
|
900
|
-
let lockRelease = null;
|
|
901
|
-
try {
|
|
902
|
-
// 1. Rate Limiting Middleware
|
|
903
|
-
const cost = OPERATION_COSTS[name] ?? 1;
|
|
904
|
-
// Use global bucket — MCP stdio has 1 client per connection
|
|
905
|
-
const rateCheck = await rateLimiter.checkLimit("global", cost);
|
|
906
|
-
if (!rateCheck.allowed) {
|
|
907
|
-
throw new Error(`Rate limit exceeded for tool: ${name}. Retry after ${rateCheck.retryAfter}ms`);
|
|
908
|
-
}
|
|
909
|
-
// 2. File Locking Middleware (for writes only)
|
|
910
|
-
if (WRITE_OPS.has(name) && args && args.filePath) {
|
|
911
|
-
lockRelease = await globalLockManager.acquireLock(String(args.filePath));
|
|
912
|
-
}
|
|
913
|
-
// 3. Execution
|
|
914
|
-
let result;
|
|
915
|
-
switch (name) {
|
|
916
|
-
case "get_semantic_repo_map":
|
|
917
|
-
result = await handleGetSemanticRepoMap(args);
|
|
918
|
-
break;
|
|
919
|
-
case "read_file_surgical":
|
|
920
|
-
result = await handleReadFileSurgical(args);
|
|
921
|
-
break;
|
|
922
|
-
case "analyze_impact":
|
|
923
|
-
result = await handleAnalyzeImpact(args);
|
|
924
|
-
break;
|
|
925
|
-
case "read_file_lines":
|
|
926
|
-
result = await handleReadFileLines(args);
|
|
927
|
-
break;
|
|
928
|
-
case "search_code_pattern":
|
|
929
|
-
result = await handleSearchCodePattern(args);
|
|
930
|
-
break;
|
|
931
|
-
case "parse_file":
|
|
932
|
-
result = await handleParseFile(args);
|
|
933
|
-
break;
|
|
934
|
-
case "write_file_surgical":
|
|
935
|
-
result = await handleWriteFileSurgical(args);
|
|
936
|
-
break;
|
|
937
|
-
case "insert_symbol":
|
|
938
|
-
result = await handleInsertSymbol(args);
|
|
939
|
-
break;
|
|
940
|
-
case "remove_symbol":
|
|
941
|
-
result = await handleRemoveSymbol(args);
|
|
942
|
-
break;
|
|
943
|
-
case "rename_symbol":
|
|
944
|
-
result = await handleRenameSymbol(args);
|
|
945
|
-
break;
|
|
946
|
-
case "rollback_file":
|
|
947
|
-
result = await handleRollbackFile(args);
|
|
948
|
-
break;
|
|
949
|
-
case "clean_backups":
|
|
950
|
-
result = await handleCleanBackups(args);
|
|
951
|
-
break;
|
|
952
|
-
case "get_server_stats":
|
|
953
|
-
result = await handleGetServerStats();
|
|
954
|
-
break;
|
|
955
|
-
case "get_cache_stats":
|
|
956
|
-
result = await handleGetCacheStats(args);
|
|
957
|
-
break;
|
|
958
|
-
case "clear_cache":
|
|
959
|
-
result = await handleClearCache(args);
|
|
960
|
-
break;
|
|
961
|
-
case "configure_file_watcher":
|
|
962
|
-
result = await handleConfigureFileWatcher(args);
|
|
963
|
-
break;
|
|
964
|
-
case "get_file_watcher_status":
|
|
965
|
-
result = await handleGetFileWatcherStatus(args);
|
|
966
|
-
break;
|
|
967
|
-
case "search_symbols":
|
|
968
|
-
result = await handleSearchSymbols(args);
|
|
969
|
-
break;
|
|
970
|
-
case "explain_symbol":
|
|
971
|
-
result = await handleExplainSymbol(args);
|
|
972
|
-
break;
|
|
973
|
-
case "batch_read":
|
|
974
|
-
result = await handleBatchRead(args);
|
|
975
|
-
break;
|
|
976
|
-
case "get_rate_limit_status":
|
|
977
|
-
result = await handleGetRateLimitStatus();
|
|
978
|
-
break;
|
|
979
|
-
default:
|
|
980
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
981
|
-
}
|
|
982
|
-
success = true;
|
|
983
|
-
return result;
|
|
984
|
-
}
|
|
985
|
-
catch (error) {
|
|
986
|
-
return {
|
|
987
|
-
content: [{ type: "text", text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` }],
|
|
988
|
-
isError: true,
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
finally {
|
|
992
|
-
const duration = Date.now() - startTime;
|
|
993
|
-
// 4. File Lock Release
|
|
994
|
-
if (lockRelease && !lockReleased) {
|
|
995
|
-
await lockRelease();
|
|
996
|
-
lockReleased = true;
|
|
997
|
-
}
|
|
998
|
-
// 5. Telemetry & Audit Logging Middleware
|
|
999
|
-
globalTelemetry.recordOperation({ toolName: name, duration, success });
|
|
1000
|
-
// Redact sensitive fields (newContent, code) from audit log
|
|
1001
|
-
const safeDetails = args ? redactSensitiveFields(args) : {};
|
|
1002
|
-
globalAuditLogger.log({
|
|
1003
|
-
level: success ? "info" : "error",
|
|
1004
|
-
operation: name,
|
|
1005
|
-
details: safeDetails,
|
|
1006
|
-
result: success ? "success" : "failure"
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
// Global shutdown hook - persist all active caches on exit
|
|
1011
30
|
const shutdownHandler = () => {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
31
|
+
globalSessionManager.shutdownAll()
|
|
32
|
+
.then(() => globalAuditLogger.stop())
|
|
33
|
+
.then(() => {
|
|
34
|
+
console.error('[shutdown] Cleanup complete. Exiting.');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
console.error('[shutdown] Session cleanup error:', err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
1015
41
|
};
|
|
1016
|
-
process.on(
|
|
1017
|
-
process.on(
|
|
1018
|
-
process.on(
|
|
42
|
+
process.on("SIGINT", shutdownHandler);
|
|
43
|
+
process.on("SIGTERM", shutdownHandler);
|
|
44
|
+
process.on("exit", shutdownHandler);
|
|
1019
45
|
main().catch((error) => {
|
|
1020
46
|
console.error("Server error:", error);
|
|
1021
47
|
process.exit(1);
|