septum 0.1.4 → 0.1.6
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/LICENSE +21 -21
- package/README.md +105 -105
- package/bin/septum.ts +4 -4
- package/package.json +63 -63
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +216 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +213 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +104 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +50 -48
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +220 -210
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +50 -38
- package/src/cli/templates/skill.ts +65 -69
- package/src/core/boundary/evaluator.ts +429 -210
- package/src/core/config/loader.ts +117 -97
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -44
- package/src/core/database/repositories/dependency.repository.ts +552 -349
- package/src/core/database/repositories/domain.repository.ts +275 -198
- package/src/core/database/repositories/file.repository.ts +117 -106
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +482 -443
- package/src/core/database/repository.ts +281 -260
- package/src/core/database/schema.ts +154 -153
- package/src/core/discovery/topology-detector.ts +486 -486
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +606 -535
- package/src/core/parser/boundary-tracker.ts +433 -288
- package/src/core/parser/extractors/base.ts +62 -62
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/frontend.ts +147 -194
- package/src/core/parser/extractors/go.ts +408 -180
- package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
- package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
- package/src/core/parser/extractors/php.ts +415 -153
- package/src/core/parser/extractors/python.ts +460 -228
- package/src/core/parser/extractors/registry.ts +70 -70
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +668 -181
- package/src/core/parser/tree-sitter.ts +41 -41
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +698 -668
- package/src/core/resolver/module-resolver.ts +479 -342
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/symbol-locator.ts +543 -543
- package/src/core/resolver/vertical-slice-tracer.ts +323 -285
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/index.ts +21 -21
- package/src/mcp/schemas.ts +695 -286
- package/src/mcp/server.ts +927 -716
- package/src/mcp/tools/check-boundary.ts +307 -152
- package/src/mcp/tools/get-domain-catalog.ts +155 -121
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +227 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
- package/src/mcp/tools/get-symbol-impact.ts +28 -26
- package/src/mcp/tools/get-symbol.ts +65 -63
- package/src/mcp/tools/locate-symbol.ts +169 -190
- package/src/mcp/tools/register-domain.ts +59 -64
- package/src/mcp/tools/register-feature.ts +91 -0
- package/src/mcp/tools/trace-vertical-slice.ts +74 -48
- package/src/types/index.ts +366 -315
- package/src/version.ts +3 -3
|
@@ -1,199 +1,328 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { SemanticSliceExtractor, VerticalSliceCandidate } from "./semantic-extractor.interface.ts";
|
|
4
|
-
|
|
5
|
-
export class McpCliSemanticExtractor implements SemanticSliceExtractor {
|
|
6
|
-
public readonly id = "mcp-cli";
|
|
7
|
-
public readonly name = "MCP Server & CLI Semantic Extractor";
|
|
8
|
-
|
|
9
|
-
public canHandle(projectRoot: string, framework?: string): boolean {
|
|
10
|
-
if (framework === "mcp-server" || framework === "cli") return true;
|
|
11
|
-
|
|
12
|
-
// Check presence of CLI or MCP directories
|
|
13
|
-
const hasMcp = fs.existsSync(path.join(projectRoot, "src/mcp/tools"));
|
|
14
|
-
const hasCli = fs.existsSync(path.join(projectRoot, "src/cli/commands"));
|
|
15
|
-
if (hasMcp || hasCli) return true;
|
|
16
|
-
|
|
17
|
-
// Check package.json for bin or MCP SDK
|
|
18
|
-
const pkgPath = path.join(projectRoot, "package.json");
|
|
19
|
-
if (fs.existsSync(pkgPath)) {
|
|
20
|
-
try {
|
|
21
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
22
|
-
if (pkg.bin || pkg.dependencies?.["@modelcontextprotocol/sdk"]) {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
} catch {}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
public extractSlices(projectRoot: string, domainId?: number): VerticalSliceCandidate[] {
|
|
32
|
-
const slices: VerticalSliceCandidate[] = [];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Detect
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { SemanticSliceExtractor, VerticalSliceCandidate } from "./semantic-extractor.interface.ts";
|
|
4
|
+
|
|
5
|
+
export class McpCliSemanticExtractor implements SemanticSliceExtractor {
|
|
6
|
+
public readonly id = "mcp-cli";
|
|
7
|
+
public readonly name = "MCP Server & CLI Semantic Extractor";
|
|
8
|
+
|
|
9
|
+
public canHandle(projectRoot: string, framework?: string): boolean {
|
|
10
|
+
if (framework === "mcp-server" || framework === "cli") return true;
|
|
11
|
+
|
|
12
|
+
// Check presence of CLI or MCP directories
|
|
13
|
+
const hasMcp = fs.existsSync(path.join(projectRoot, "src/mcp/tools"));
|
|
14
|
+
const hasCli = fs.existsSync(path.join(projectRoot, "src/cli/commands"));
|
|
15
|
+
if (hasMcp || hasCli) return true;
|
|
16
|
+
|
|
17
|
+
// Check package.json for bin or MCP SDK
|
|
18
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
19
|
+
if (fs.existsSync(pkgPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
22
|
+
if (pkg.bin || pkg.dependencies?.["@modelcontextprotocol/sdk"]) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public extractSlices(projectRoot: string, domainId?: number): VerticalSliceCandidate[] {
|
|
32
|
+
const slices: VerticalSliceCandidate[] = [];
|
|
33
|
+
const { toolPrefix, cliBinName } = this.resolveProjectNaming(projectRoot);
|
|
34
|
+
const registeredTools = this.scanRegisteredMcpToolNames(projectRoot);
|
|
35
|
+
|
|
36
|
+
// 1. Extract MCP Tools
|
|
37
|
+
const mcpToolsDir = path.join(projectRoot, "src/mcp/tools");
|
|
38
|
+
if (fs.existsSync(mcpToolsDir)) {
|
|
39
|
+
try {
|
|
40
|
+
const toolFiles = fs
|
|
41
|
+
.readdirSync(mcpToolsDir)
|
|
42
|
+
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
|
43
|
+
|
|
44
|
+
for (const file of toolFiles) {
|
|
45
|
+
const fullPath = path.join(mcpToolsDir, file);
|
|
46
|
+
const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
47
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
48
|
+
const baseName = path.basename(file, path.extname(file));
|
|
49
|
+
const snakeBase = baseName.replace(/-/g, "_");
|
|
50
|
+
const toolIdentifier =
|
|
51
|
+
registeredTools.get(snakeBase) || `${toolPrefix}_${snakeBase}`;
|
|
52
|
+
|
|
53
|
+
// Detect handler function (both sync and async) and its 1-indexed line number
|
|
54
|
+
const handlerMatch = /export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/.exec(content);
|
|
55
|
+
const classMatch = /export\s+class\s+([A-Za-z0-9_]+)/.exec(content);
|
|
56
|
+
const actionName = handlerMatch ? handlerMatch[1] : `handle_${snakeBase}`;
|
|
57
|
+
const controllerClass = classMatch ? classMatch[1] : baseName;
|
|
58
|
+
const handlerLine =
|
|
59
|
+
handlerMatch && handlerMatch.index !== undefined
|
|
60
|
+
? content.slice(0, handlerMatch.index).split("\n").length
|
|
61
|
+
: 1;
|
|
62
|
+
|
|
63
|
+
// Detect args interface and its line number
|
|
64
|
+
const argsMatch = /(?:export\s+)?interface\s+([a-zA-Z0-9_]+Args)\b/.exec(content);
|
|
65
|
+
const formRequestClass = argsMatch ? argsMatch[1] : null;
|
|
66
|
+
const argsLine =
|
|
67
|
+
argsMatch && argsMatch.index !== undefined
|
|
68
|
+
? content.slice(0, argsMatch.index).split("\n").length
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
// Detect service/repository dependency and its actual file path
|
|
72
|
+
const serviceDep = this.resolveServiceDependency(projectRoot, fullPath, content);
|
|
73
|
+
|
|
74
|
+
const mcpChain: Array<{
|
|
75
|
+
stage: string;
|
|
76
|
+
symbol: string;
|
|
77
|
+
file?: string;
|
|
78
|
+
line?: number;
|
|
79
|
+
description?: string;
|
|
80
|
+
}> = [
|
|
81
|
+
{
|
|
82
|
+
stage: "ingress",
|
|
83
|
+
symbol: `tool:${toolIdentifier}`,
|
|
84
|
+
file: relPath,
|
|
85
|
+
line: handlerLine,
|
|
86
|
+
description: `MCP Tool Endpoint: ${toolIdentifier}`,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (formRequestClass) {
|
|
91
|
+
mcpChain.push({
|
|
92
|
+
stage: "validation",
|
|
93
|
+
symbol: formRequestClass,
|
|
94
|
+
file: relPath,
|
|
95
|
+
...(argsLine ? { line: argsLine } : {}),
|
|
96
|
+
description: "MCP Argument Schema",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
mcpChain.push({
|
|
101
|
+
stage: "controller",
|
|
102
|
+
symbol: classMatch ? `${controllerClass}::${actionName}` : actionName,
|
|
103
|
+
file: relPath,
|
|
104
|
+
line: handlerLine,
|
|
105
|
+
description: "Tool Handler Execution",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (serviceDep) {
|
|
109
|
+
mcpChain.push({
|
|
110
|
+
stage: "service",
|
|
111
|
+
symbol: serviceDep.symbol,
|
|
112
|
+
...(serviceDep.file ? { file: serviceDep.file } : {}),
|
|
113
|
+
description: "Core Engine Service",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
slices.push({
|
|
118
|
+
domain_id: domainId ?? null,
|
|
119
|
+
feature_key: `mcp.${baseName}`,
|
|
120
|
+
http_method: "MCP",
|
|
121
|
+
route_uri: `tool:${toolIdentifier}`,
|
|
122
|
+
route_name: toolIdentifier,
|
|
123
|
+
controller_class: controllerClass,
|
|
124
|
+
action_name: actionName,
|
|
125
|
+
controller_file: relPath,
|
|
126
|
+
controller_line: handlerLine,
|
|
127
|
+
architecture_style: "rpc",
|
|
128
|
+
entry_kind: "mcp_tool",
|
|
129
|
+
execution_chain_json: JSON.stringify(mcpChain),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2. Extract CLI Commands
|
|
136
|
+
const cliCommandsDir = path.join(projectRoot, "src/cli/commands");
|
|
137
|
+
if (fs.existsSync(cliCommandsDir)) {
|
|
138
|
+
try {
|
|
139
|
+
const commandFiles = fs
|
|
140
|
+
.readdirSync(cliCommandsDir)
|
|
141
|
+
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
|
142
|
+
|
|
143
|
+
for (const file of commandFiles) {
|
|
144
|
+
const fullPath = path.join(cliCommandsDir, file);
|
|
145
|
+
const relPath = path.relative(projectRoot, fullPath).replace(/\\/g, "/");
|
|
146
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
147
|
+
const commandName = path.basename(file, path.extname(file));
|
|
148
|
+
|
|
149
|
+
const handlerMatch = /export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/.exec(content);
|
|
150
|
+
const classMatch = /export\s+class\s+([A-Za-z0-9_]+)/.exec(content);
|
|
151
|
+
const actionName = handlerMatch
|
|
152
|
+
? handlerMatch[1]
|
|
153
|
+
: `handle${commandName.charAt(0).toUpperCase() + commandName.slice(1)}Command`;
|
|
154
|
+
const controllerClass = classMatch ? classMatch[1] : commandName;
|
|
155
|
+
const handlerLine =
|
|
156
|
+
handlerMatch && handlerMatch.index !== undefined
|
|
157
|
+
? content.slice(0, handlerMatch.index).split("\n").length
|
|
158
|
+
: 1;
|
|
159
|
+
|
|
160
|
+
// Detect options / args
|
|
161
|
+
const optionsMatch = /(?:export\s+)?interface\s+([a-zA-Z0-9_]+(?:Options|Args))\b/.exec(content);
|
|
162
|
+
const formRequestClass = optionsMatch ? optionsMatch[1] : null;
|
|
163
|
+
const optionsLine =
|
|
164
|
+
optionsMatch && optionsMatch.index !== undefined
|
|
165
|
+
? content.slice(0, optionsMatch.index).split("\n").length
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
168
|
+
const serviceDep = this.resolveServiceDependency(projectRoot, fullPath, content);
|
|
169
|
+
|
|
170
|
+
const cliChain: Array<{
|
|
171
|
+
stage: string;
|
|
172
|
+
symbol: string;
|
|
173
|
+
file?: string;
|
|
174
|
+
line?: number;
|
|
175
|
+
description?: string;
|
|
176
|
+
}> = [
|
|
177
|
+
{
|
|
178
|
+
stage: "ingress",
|
|
179
|
+
symbol: `${cliBinName} ${commandName}`,
|
|
180
|
+
file: relPath,
|
|
181
|
+
line: handlerLine,
|
|
182
|
+
description: `CLI Command Entrypoint: ${cliBinName} ${commandName}`,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
if (formRequestClass) {
|
|
187
|
+
cliChain.push({
|
|
188
|
+
stage: "validation",
|
|
189
|
+
symbol: formRequestClass,
|
|
190
|
+
file: relPath,
|
|
191
|
+
...(optionsLine ? { line: optionsLine } : {}),
|
|
192
|
+
description: "CLI Options Interface",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
cliChain.push({
|
|
197
|
+
stage: "controller",
|
|
198
|
+
symbol: classMatch ? `${controllerClass}::${actionName}` : actionName,
|
|
199
|
+
file: relPath,
|
|
200
|
+
line: handlerLine,
|
|
201
|
+
description: "Command Execution Handler",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (serviceDep) {
|
|
205
|
+
cliChain.push({
|
|
206
|
+
stage: "service",
|
|
207
|
+
symbol: serviceDep.symbol,
|
|
208
|
+
...(serviceDep.file ? { file: serviceDep.file } : {}),
|
|
209
|
+
description: "Core Engine Service",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
slices.push({
|
|
214
|
+
domain_id: domainId ?? null,
|
|
215
|
+
feature_key: `cli.${commandName}`,
|
|
216
|
+
http_method: "CLI",
|
|
217
|
+
route_uri: `command:${commandName}`,
|
|
218
|
+
route_name: `${cliBinName} ${commandName}`,
|
|
219
|
+
controller_class: controllerClass,
|
|
220
|
+
action_name: actionName,
|
|
221
|
+
controller_file: relPath,
|
|
222
|
+
controller_line: handlerLine,
|
|
223
|
+
architecture_style: "cli",
|
|
224
|
+
entry_kind: "cli_command",
|
|
225
|
+
execution_chain_json: JSON.stringify(cliChain),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return slices;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private resolveProjectNaming(projectRoot: string): { toolPrefix: string; cliBinName: string } {
|
|
235
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
236
|
+
if (fs.existsSync(pkgPath)) {
|
|
237
|
+
try {
|
|
238
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
239
|
+
const rawName = typeof pkg.name === "string" ? pkg.name.replace(/^@[^/]+\//, "") : "mcp";
|
|
240
|
+
const toolPrefix = rawName.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase() || "mcp";
|
|
241
|
+
let cliBinName = rawName;
|
|
242
|
+
if (typeof pkg.bin === "string") {
|
|
243
|
+
cliBinName = rawName;
|
|
244
|
+
} else if (pkg.bin && typeof pkg.bin === "object") {
|
|
245
|
+
const firstBin = Object.keys(pkg.bin)[0];
|
|
246
|
+
if (firstBin) cliBinName = firstBin;
|
|
247
|
+
}
|
|
248
|
+
return { toolPrefix, cliBinName };
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
return { toolPrefix: "mcp", cliBinName: "cli" };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private scanRegisteredMcpToolNames(projectRoot: string): Map<string, string> {
|
|
255
|
+
const map = new Map<string, string>();
|
|
256
|
+
const serverPath = path.join(projectRoot, "src/mcp/server.ts");
|
|
257
|
+
if (!fs.existsSync(serverPath)) return map;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const content = fs.readFileSync(serverPath, "utf-8");
|
|
261
|
+
const nameRegex = /name:\s*['"]([a-zA-Z0-9_-]+)['"]/g;
|
|
262
|
+
let match: RegExpExecArray | null;
|
|
263
|
+
while ((match = nameRegex.exec(content)) !== null) {
|
|
264
|
+
const fullToolName = match[1];
|
|
265
|
+
const normalized = fullToolName.replace(/-/g, "_");
|
|
266
|
+
map.set(normalized, fullToolName);
|
|
267
|
+
const withoutPrefix = normalized.replace(/^[a-z0-9]+_/, "");
|
|
268
|
+
if (withoutPrefix) {
|
|
269
|
+
map.set(withoutPrefix, fullToolName);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
273
|
+
return map;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private resolveServiceDependency(
|
|
277
|
+
projectRoot: string,
|
|
278
|
+
sourceFile: string,
|
|
279
|
+
content: string
|
|
280
|
+
): { symbol: string; file?: string } | null {
|
|
281
|
+
const importRegex = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
282
|
+
let match: RegExpExecArray | null;
|
|
283
|
+
|
|
284
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
285
|
+
const clause = match[1];
|
|
286
|
+
const spec = match[2];
|
|
287
|
+
const identifiers = clause
|
|
288
|
+
.split(",")
|
|
289
|
+
.map((s) => s.trim().split(/\s+as\s+/)[0].trim())
|
|
290
|
+
.filter(Boolean);
|
|
291
|
+
|
|
292
|
+
for (const id of identifiers) {
|
|
293
|
+
if (
|
|
294
|
+
/(?:Repository|Service|Manager|Loader|Pipeline|Evaluator|Tracer|Detector|Engine|Store)$/.test(
|
|
295
|
+
id
|
|
296
|
+
)
|
|
297
|
+
) {
|
|
298
|
+
const resolvedFile = this.resolveImportFile(projectRoot, sourceFile, spec);
|
|
299
|
+
return {
|
|
300
|
+
symbol: id,
|
|
301
|
+
...(resolvedFile ? { file: resolvedFile } : {}),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private resolveImportFile(projectRoot: string, sourceFile: string, importSpec: string): string | undefined {
|
|
311
|
+
if (!importSpec.startsWith(".")) return undefined;
|
|
312
|
+
const baseResolved = path.resolve(path.dirname(sourceFile), importSpec);
|
|
313
|
+
const candidates = [
|
|
314
|
+
baseResolved,
|
|
315
|
+
`${baseResolved}.ts`,
|
|
316
|
+
`${baseResolved}.js`,
|
|
317
|
+
baseResolved.replace(/\.js$/, ".ts"),
|
|
318
|
+
path.join(baseResolved, "index.ts"),
|
|
319
|
+
];
|
|
320
|
+
for (const c of candidates) {
|
|
321
|
+
if (fs.existsSync(c) && fs.statSync(c).isFile()) {
|
|
322
|
+
return path.relative(projectRoot, c).replace(/\\/g, "/");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|