@navigation-agent/mcp-server 0.1.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/dist/app/createMcpServer.d.ts +18 -0
- package/dist/app/createMcpServer.js +152 -0
- package/dist/bin/navigation-mcp.d.ts +2 -0
- package/dist/bin/navigation-mcp.js +36 -0
- package/dist/contracts/public/code.d.ts +1106 -0
- package/dist/contracts/public/code.js +523 -0
- package/dist/contracts/public/common.d.ts +28 -0
- package/dist/contracts/public/common.js +23 -0
- package/dist/engine/protocol.d.ts +290 -0
- package/dist/engine/protocol.js +13 -0
- package/dist/engine/rustEngineClient.d.ts +16 -0
- package/dist/engine/rustEngineClient.js +138 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/services/findSymbolService.d.ts +11 -0
- package/dist/services/findSymbolService.js +224 -0
- package/dist/services/inspectTreeService.d.ts +11 -0
- package/dist/services/inspectTreeService.js +191 -0
- package/dist/services/listEndpointsService.d.ts +11 -0
- package/dist/services/listEndpointsService.js +233 -0
- package/dist/services/searchTextService.d.ts +11 -0
- package/dist/services/searchTextService.js +233 -0
- package/dist/services/traceCallersService.d.ts +11 -0
- package/dist/services/traceCallersService.js +256 -0
- package/dist/services/traceSymbolService.d.ts +11 -0
- package/dist/services/traceSymbolService.js +231 -0
- package/dist/tools/registerCodeTools.d.ts +20 -0
- package/dist/tools/registerCodeTools.js +188 -0
- package/package.json +51 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { normalizeFindSymbolInput, } from "../contracts/public/code.js";
|
|
2
|
+
import { createResponseMeta, } from "../contracts/public/common.js";
|
|
3
|
+
import { nextRequestId, } from "../engine/protocol.js";
|
|
4
|
+
const TOOL_NAME = "code.find_symbol";
|
|
5
|
+
export function createFindSymbolService(options) {
|
|
6
|
+
return {
|
|
7
|
+
async execute(input) {
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
response = await options.engineClient.request({
|
|
11
|
+
id: nextRequestId(),
|
|
12
|
+
capability: "workspace.find_symbol",
|
|
13
|
+
workspaceRoot: options.workspaceRoot,
|
|
14
|
+
payload: {
|
|
15
|
+
symbol: input.symbol,
|
|
16
|
+
path: input.path ?? null,
|
|
17
|
+
analyzerLanguage: resolveAnalyzerLanguage(input.language, input.framework),
|
|
18
|
+
publicLanguageFilter: resolveEffectiveLanguage(input.language, input.framework),
|
|
19
|
+
kind: input.kind,
|
|
20
|
+
matchMode: input.match,
|
|
21
|
+
limit: input.limit,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return buildEngineFailureResponse(input, error);
|
|
27
|
+
}
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
return buildMappedErrorResponse(input, response.error.code, response.error.message, response.error.details, response.error.retryable);
|
|
30
|
+
}
|
|
31
|
+
return buildSuccessResponse(input, response.result);
|
|
32
|
+
},
|
|
33
|
+
async validateAndExecute(payload) {
|
|
34
|
+
const normalized = normalizeFindSymbolInput(payload);
|
|
35
|
+
if (!normalized.ok) {
|
|
36
|
+
return buildValidationErrorResponse(normalized.issues);
|
|
37
|
+
}
|
|
38
|
+
return this.execute(normalized.value);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function buildSuccessResponse(input, result) {
|
|
43
|
+
const effectiveLanguage = resolveEffectiveLanguage(input.language, input.framework);
|
|
44
|
+
const count = result.totalMatched;
|
|
45
|
+
const returnedCount = result.items.length;
|
|
46
|
+
return {
|
|
47
|
+
tool: TOOL_NAME,
|
|
48
|
+
status: result.truncated ? "partial" : "ok",
|
|
49
|
+
summary: buildSummary(input.symbol, count, result.truncated),
|
|
50
|
+
data: {
|
|
51
|
+
count,
|
|
52
|
+
returnedCount,
|
|
53
|
+
totalMatched: result.totalMatched,
|
|
54
|
+
items: result.items.map((item) => ({
|
|
55
|
+
symbol: item.symbol,
|
|
56
|
+
kind: item.kind,
|
|
57
|
+
path: item.path,
|
|
58
|
+
line: item.line,
|
|
59
|
+
language: item.language,
|
|
60
|
+
})),
|
|
61
|
+
},
|
|
62
|
+
errors: result.truncated
|
|
63
|
+
? [
|
|
64
|
+
{
|
|
65
|
+
code: "RESULT_TRUNCATED",
|
|
66
|
+
message: `Result set exceeded the requested limit of ${input.limit} items.`,
|
|
67
|
+
retryable: false,
|
|
68
|
+
suggestion: "Increase limit or narrow the path/language filter.",
|
|
69
|
+
details: {
|
|
70
|
+
returned: returnedCount,
|
|
71
|
+
total: result.totalMatched,
|
|
72
|
+
limit: input.limit,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
: [],
|
|
77
|
+
meta: createResponseMeta({
|
|
78
|
+
query: { ...input },
|
|
79
|
+
resolvedPath: result.resolvedPath,
|
|
80
|
+
truncated: result.truncated,
|
|
81
|
+
counts: {
|
|
82
|
+
returnedCount,
|
|
83
|
+
totalMatched: result.totalMatched,
|
|
84
|
+
},
|
|
85
|
+
detection: {
|
|
86
|
+
effectiveLanguage,
|
|
87
|
+
framework: input.framework ?? null,
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function buildValidationErrorResponse(issues) {
|
|
93
|
+
return {
|
|
94
|
+
tool: TOOL_NAME,
|
|
95
|
+
status: "error",
|
|
96
|
+
summary: "Request validation failed.",
|
|
97
|
+
data: { count: 0, returnedCount: 0, totalMatched: 0, items: [] },
|
|
98
|
+
errors: [
|
|
99
|
+
{
|
|
100
|
+
code: "INVALID_INPUT",
|
|
101
|
+
message: "One or more input fields are invalid.",
|
|
102
|
+
retryable: false,
|
|
103
|
+
suggestion: "Correct the invalid fields and try again.",
|
|
104
|
+
details: { issues },
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
meta: createResponseMeta({ query: {} }),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function buildMappedErrorResponse(input, code, message, details, retryable) {
|
|
111
|
+
const query = { ...input };
|
|
112
|
+
if (code === "FILE_NOT_FOUND") {
|
|
113
|
+
return {
|
|
114
|
+
tool: TOOL_NAME,
|
|
115
|
+
status: "error",
|
|
116
|
+
summary: "Path not found.",
|
|
117
|
+
data: { count: 0, returnedCount: 0, totalMatched: 0, items: [] },
|
|
118
|
+
errors: [
|
|
119
|
+
{
|
|
120
|
+
code,
|
|
121
|
+
message,
|
|
122
|
+
retryable,
|
|
123
|
+
suggestion: "Provide an existing file or directory path inside the workspace root.",
|
|
124
|
+
details,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
meta: createResponseMeta({ query }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (code === "PATH_OUTSIDE_WORKSPACE") {
|
|
131
|
+
return {
|
|
132
|
+
tool: TOOL_NAME,
|
|
133
|
+
status: "error",
|
|
134
|
+
summary: "Path validation failed.",
|
|
135
|
+
data: { count: 0, returnedCount: 0, totalMatched: 0, items: [] },
|
|
136
|
+
errors: [
|
|
137
|
+
{
|
|
138
|
+
code,
|
|
139
|
+
message,
|
|
140
|
+
retryable,
|
|
141
|
+
suggestion: "Use a path inside the workspace root or omit the path filter.",
|
|
142
|
+
details,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
meta: createResponseMeta({ query }),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (code === "UNSUPPORTED_CAPABILITY") {
|
|
149
|
+
return {
|
|
150
|
+
tool: TOOL_NAME,
|
|
151
|
+
status: "error",
|
|
152
|
+
summary: "Symbol analysis failed.",
|
|
153
|
+
data: { count: 0, returnedCount: 0, totalMatched: 0, items: [] },
|
|
154
|
+
errors: [
|
|
155
|
+
{
|
|
156
|
+
code: "BACKEND_EXECUTION_FAILED",
|
|
157
|
+
message,
|
|
158
|
+
retryable,
|
|
159
|
+
suggestion: "Verify the engine supports workspace.find_symbol and retry.",
|
|
160
|
+
details,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
meta: createResponseMeta({ query }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
tool: TOOL_NAME,
|
|
168
|
+
status: "error",
|
|
169
|
+
summary: "Symbol analysis failed.",
|
|
170
|
+
data: { count: 0, returnedCount: 0, totalMatched: 0, items: [] },
|
|
171
|
+
errors: [
|
|
172
|
+
{
|
|
173
|
+
code: code === "BACKEND_EXECUTION_FAILED" ? code : "BACKEND_EXECUTION_FAILED",
|
|
174
|
+
message,
|
|
175
|
+
retryable,
|
|
176
|
+
details,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
meta: createResponseMeta({ query }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function buildEngineFailureResponse(input, error) {
|
|
183
|
+
return buildMappedErrorResponse(input, "BACKEND_EXECUTION_FAILED", error instanceof Error ? error.message : String(error), {}, false);
|
|
184
|
+
}
|
|
185
|
+
function resolveEffectiveLanguage(language, framework) {
|
|
186
|
+
if (language) {
|
|
187
|
+
return language;
|
|
188
|
+
}
|
|
189
|
+
if (framework === "react-router") {
|
|
190
|
+
return "typescript";
|
|
191
|
+
}
|
|
192
|
+
if (framework === "spring") {
|
|
193
|
+
return "java";
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function resolveAnalyzerLanguage(language, framework) {
|
|
198
|
+
const effective = resolveEffectiveLanguage(language, framework);
|
|
199
|
+
if (effective === "java") {
|
|
200
|
+
return "java";
|
|
201
|
+
}
|
|
202
|
+
if (effective === "python") {
|
|
203
|
+
return "python";
|
|
204
|
+
}
|
|
205
|
+
if (effective === "rust") {
|
|
206
|
+
return "rust";
|
|
207
|
+
}
|
|
208
|
+
if (effective === "typescript" || effective === "javascript") {
|
|
209
|
+
return "typescript";
|
|
210
|
+
}
|
|
211
|
+
return "auto";
|
|
212
|
+
}
|
|
213
|
+
function buildSummary(symbol, count, truncated) {
|
|
214
|
+
if (count === 0) {
|
|
215
|
+
return `No symbol definitions found for '${symbol}'.`;
|
|
216
|
+
}
|
|
217
|
+
if (truncated) {
|
|
218
|
+
return `Found ${count} symbol definitions for '${symbol}' and returned a truncated subset.`;
|
|
219
|
+
}
|
|
220
|
+
if (count === 1) {
|
|
221
|
+
return `Found 1 symbol definition for '${symbol}'.`;
|
|
222
|
+
}
|
|
223
|
+
return `Found ${count} symbol definitions for '${symbol}'.`;
|
|
224
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type InspectTreeData, type InspectTreeInput } from "../contracts/public/code.ts";
|
|
2
|
+
import { type ResponseEnvelope } from "../contracts/public/common.ts";
|
|
3
|
+
import type { EngineClient } from "../engine/rustEngineClient.ts";
|
|
4
|
+
export interface InspectTreeService {
|
|
5
|
+
execute(input: InspectTreeInput): Promise<ResponseEnvelope<InspectTreeData>>;
|
|
6
|
+
validateAndExecute(payload: Record<string, unknown>): Promise<ResponseEnvelope<InspectTreeData>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createInspectTreeService(options: {
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
engineClient: EngineClient;
|
|
11
|
+
}): InspectTreeService;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { normalizeInspectTreeInput, } from "../contracts/public/code.js";
|
|
2
|
+
import { createResponseMeta, } from "../contracts/public/common.js";
|
|
3
|
+
import { nextRequestId, } from "../engine/protocol.js";
|
|
4
|
+
export function createInspectTreeService(options) {
|
|
5
|
+
return {
|
|
6
|
+
async execute(input) {
|
|
7
|
+
let response;
|
|
8
|
+
try {
|
|
9
|
+
response = await options.engineClient.request({
|
|
10
|
+
id: nextRequestId(),
|
|
11
|
+
capability: "workspace.inspect_tree",
|
|
12
|
+
workspaceRoot: options.workspaceRoot,
|
|
13
|
+
payload: {
|
|
14
|
+
path: input.path ?? null,
|
|
15
|
+
maxDepth: input.max_depth,
|
|
16
|
+
extensions: input.extensions,
|
|
17
|
+
filePattern: input.file_pattern ?? null,
|
|
18
|
+
includeStats: input.include_stats,
|
|
19
|
+
includeHidden: input.include_hidden,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return buildEngineFailureResponse(input, error);
|
|
25
|
+
}
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
return buildMappedErrorResponse(input, response.error.code, response.error.message, response.error.details, response.error.retryable);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
tool: "code.inspect_tree",
|
|
31
|
+
status: response.result.truncated ? "partial" : "ok",
|
|
32
|
+
summary: buildSummary(response.result.root, response.result.items.length, response.result.truncated),
|
|
33
|
+
data: mapInspectTreeResult(response.result),
|
|
34
|
+
errors: response.result.truncated
|
|
35
|
+
? [
|
|
36
|
+
{
|
|
37
|
+
code: "RESULT_TRUNCATED",
|
|
38
|
+
message: `Tree inspection hit the safety cap of ${response.result.maxItems} items.`,
|
|
39
|
+
retryable: false,
|
|
40
|
+
details: {
|
|
41
|
+
returned: response.result.items.length,
|
|
42
|
+
maxItems: response.result.maxItems,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [],
|
|
47
|
+
meta: createResponseMeta({
|
|
48
|
+
query: { ...input },
|
|
49
|
+
resolvedPath: response.result.root,
|
|
50
|
+
truncated: response.result.truncated,
|
|
51
|
+
counts: {
|
|
52
|
+
returnedCount: response.result.items.length,
|
|
53
|
+
totalMatched: response.result.truncated
|
|
54
|
+
? null
|
|
55
|
+
: response.result.items.length,
|
|
56
|
+
},
|
|
57
|
+
detection: {
|
|
58
|
+
includeHidden: input.include_hidden ? "true" : "false",
|
|
59
|
+
stats: input.include_stats ? "true" : "false",
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
async validateAndExecute(payload) {
|
|
65
|
+
const normalized = normalizeInspectTreeInput(payload);
|
|
66
|
+
if (!normalized.ok) {
|
|
67
|
+
return buildValidationErrorResponse(normalized.issues);
|
|
68
|
+
}
|
|
69
|
+
return this.execute(normalized.value);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function mapInspectTreeResult(result) {
|
|
74
|
+
return {
|
|
75
|
+
root: result.root,
|
|
76
|
+
entryCount: result.items.length,
|
|
77
|
+
items: result.items.map((item) => ({
|
|
78
|
+
path: item.path,
|
|
79
|
+
name: item.name,
|
|
80
|
+
type: item.type,
|
|
81
|
+
depth: item.depth,
|
|
82
|
+
extension: item.extension ?? null,
|
|
83
|
+
stats: item.stats ?? null,
|
|
84
|
+
})),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildValidationErrorResponse(issues) {
|
|
88
|
+
return {
|
|
89
|
+
tool: "code.inspect_tree",
|
|
90
|
+
status: "error",
|
|
91
|
+
summary: "Request validation failed.",
|
|
92
|
+
data: { root: ".", entryCount: 0, items: [] },
|
|
93
|
+
errors: [
|
|
94
|
+
{
|
|
95
|
+
code: "INVALID_INPUT",
|
|
96
|
+
message: "One or more input fields are invalid.",
|
|
97
|
+
retryable: false,
|
|
98
|
+
suggestion: "Correct the invalid fields and try again.",
|
|
99
|
+
details: { issues },
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
meta: createResponseMeta({ query: {} }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function buildMappedErrorResponse(input, code, message, details, retryable) {
|
|
106
|
+
const query = { ...input };
|
|
107
|
+
if (code === "FILE_NOT_FOUND") {
|
|
108
|
+
return {
|
|
109
|
+
tool: "code.inspect_tree",
|
|
110
|
+
status: "error",
|
|
111
|
+
summary: "Path not found.",
|
|
112
|
+
data: { root: ".", entryCount: 0, items: [] },
|
|
113
|
+
errors: [
|
|
114
|
+
{
|
|
115
|
+
code,
|
|
116
|
+
message,
|
|
117
|
+
retryable,
|
|
118
|
+
suggestion: "Provide an existing file or directory path inside the workspace root.",
|
|
119
|
+
details,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
meta: createResponseMeta({ query }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (code === "PATH_OUTSIDE_WORKSPACE") {
|
|
126
|
+
return {
|
|
127
|
+
tool: "code.inspect_tree",
|
|
128
|
+
status: "error",
|
|
129
|
+
summary: "Path validation failed.",
|
|
130
|
+
data: { root: ".", entryCount: 0, items: [] },
|
|
131
|
+
errors: [
|
|
132
|
+
{
|
|
133
|
+
code,
|
|
134
|
+
message,
|
|
135
|
+
retryable,
|
|
136
|
+
suggestion: "Use a path inside the workspace root or omit the path filter.",
|
|
137
|
+
details,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
meta: createResponseMeta({ query }),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (code === "UNSUPPORTED_CAPABILITY") {
|
|
144
|
+
return {
|
|
145
|
+
tool: "code.inspect_tree",
|
|
146
|
+
status: "error",
|
|
147
|
+
summary: "Inspect tree execution failed.",
|
|
148
|
+
data: { root: input.path ?? ".", entryCount: 0, items: [] },
|
|
149
|
+
errors: [
|
|
150
|
+
{
|
|
151
|
+
code: "BACKEND_EXECUTION_FAILED",
|
|
152
|
+
message,
|
|
153
|
+
retryable,
|
|
154
|
+
suggestion: "Verify the engine supports workspace.inspect_tree and retry.",
|
|
155
|
+
details,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
meta: createResponseMeta({ query }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
tool: "code.inspect_tree",
|
|
163
|
+
status: "error",
|
|
164
|
+
summary: "Inspect tree execution failed.",
|
|
165
|
+
data: { root: input.path ?? ".", entryCount: 0, items: [] },
|
|
166
|
+
errors: [
|
|
167
|
+
{
|
|
168
|
+
code: code === "BACKEND_EXECUTION_FAILED" ? code : "BACKEND_EXECUTION_FAILED",
|
|
169
|
+
message,
|
|
170
|
+
retryable,
|
|
171
|
+
details,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
meta: createResponseMeta({ query }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function buildEngineFailureResponse(input, error) {
|
|
178
|
+
return buildMappedErrorResponse(input, "BACKEND_EXECUTION_FAILED", error instanceof Error ? error.message : String(error), {}, false);
|
|
179
|
+
}
|
|
180
|
+
function buildSummary(root, entryCount, truncated) {
|
|
181
|
+
if (entryCount === 0) {
|
|
182
|
+
return `No tree entries found under '${root}'.`;
|
|
183
|
+
}
|
|
184
|
+
if (truncated) {
|
|
185
|
+
return `Inspected ${entryCount} tree entries under '${root}' and returned a truncated subset.`;
|
|
186
|
+
}
|
|
187
|
+
if (entryCount === 1) {
|
|
188
|
+
return `Inspected 1 tree entry under '${root}'.`;
|
|
189
|
+
}
|
|
190
|
+
return `Inspected ${entryCount} tree entries under '${root}'.`;
|
|
191
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ListEndpointsData, type ListEndpointsInput } from "../contracts/public/code.ts";
|
|
2
|
+
import { type ResponseEnvelope } from "../contracts/public/common.ts";
|
|
3
|
+
import type { EngineClient } from "../engine/rustEngineClient.ts";
|
|
4
|
+
export interface ListEndpointsService {
|
|
5
|
+
execute(input: ListEndpointsInput): Promise<ResponseEnvelope<ListEndpointsData>>;
|
|
6
|
+
validateAndExecute(payload: Record<string, unknown>): Promise<ResponseEnvelope<ListEndpointsData>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createListEndpointsService(options: {
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
engineClient: EngineClient;
|
|
11
|
+
}): ListEndpointsService;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { normalizeListEndpointsInput, } from "../contracts/public/code.js";
|
|
2
|
+
import { createResponseMeta, } from "../contracts/public/common.js";
|
|
3
|
+
import { nextRequestId, } from "../engine/protocol.js";
|
|
4
|
+
const TOOL_NAME = "code.list_endpoints";
|
|
5
|
+
export function createListEndpointsService(options) {
|
|
6
|
+
return {
|
|
7
|
+
async execute(input) {
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
response = await options.engineClient.request({
|
|
11
|
+
id: nextRequestId(),
|
|
12
|
+
capability: "workspace.list_endpoints",
|
|
13
|
+
workspaceRoot: options.workspaceRoot,
|
|
14
|
+
payload: {
|
|
15
|
+
path: input.path ?? null,
|
|
16
|
+
analyzerLanguage: resolveAnalyzerLanguage(input.language, input.framework),
|
|
17
|
+
publicLanguageFilter: resolveEffectiveLanguage(input.language, input.framework),
|
|
18
|
+
publicFrameworkFilter: input.framework ?? null,
|
|
19
|
+
kind: input.kind,
|
|
20
|
+
limit: input.limit,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return buildEngineFailureResponse(input, error);
|
|
26
|
+
}
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
return buildMappedErrorResponse(input, response.error.code, response.error.message, response.error.details, response.error.retryable);
|
|
29
|
+
}
|
|
30
|
+
return buildSuccessResponse(input, response.result);
|
|
31
|
+
},
|
|
32
|
+
async validateAndExecute(payload) {
|
|
33
|
+
const normalized = normalizeListEndpointsInput(payload);
|
|
34
|
+
if (!normalized.ok) {
|
|
35
|
+
return buildValidationErrorResponse(normalized.issues);
|
|
36
|
+
}
|
|
37
|
+
return this.execute(normalized.value);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildSuccessResponse(input, result) {
|
|
42
|
+
const effectiveLanguage = resolveEffectiveLanguage(input.language, input.framework);
|
|
43
|
+
const totalCount = result.totalMatched;
|
|
44
|
+
const returnedCount = result.items.length;
|
|
45
|
+
return {
|
|
46
|
+
tool: TOOL_NAME,
|
|
47
|
+
status: result.truncated ? "partial" : "ok",
|
|
48
|
+
summary: buildSummary(totalCount, result.truncated, input.kind),
|
|
49
|
+
data: {
|
|
50
|
+
totalCount,
|
|
51
|
+
returnedCount,
|
|
52
|
+
counts: mapCounts(result.counts),
|
|
53
|
+
items: result.items.map((item) => ({
|
|
54
|
+
name: item.name,
|
|
55
|
+
kind: item.kind,
|
|
56
|
+
path: item.path ?? null,
|
|
57
|
+
file: item.file,
|
|
58
|
+
line: item.line,
|
|
59
|
+
language: item.language ?? null,
|
|
60
|
+
framework: item.framework ?? null,
|
|
61
|
+
})),
|
|
62
|
+
},
|
|
63
|
+
errors: result.truncated
|
|
64
|
+
? [
|
|
65
|
+
{
|
|
66
|
+
code: "RESULT_TRUNCATED",
|
|
67
|
+
message: `Result set exceeded the requested limit of ${input.limit} items.`,
|
|
68
|
+
retryable: false,
|
|
69
|
+
suggestion: "Increase limit or narrow the path/language/framework filter.",
|
|
70
|
+
details: {
|
|
71
|
+
returned: returnedCount,
|
|
72
|
+
total: result.totalMatched,
|
|
73
|
+
limit: input.limit,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
: [],
|
|
78
|
+
meta: createResponseMeta({
|
|
79
|
+
query: { ...input },
|
|
80
|
+
resolvedPath: result.resolvedPath,
|
|
81
|
+
truncated: result.truncated,
|
|
82
|
+
counts: {
|
|
83
|
+
returnedCount,
|
|
84
|
+
totalMatched: result.totalMatched,
|
|
85
|
+
},
|
|
86
|
+
detection: {
|
|
87
|
+
effectiveLanguage,
|
|
88
|
+
framework: input.framework ?? null,
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function mapCounts(counts) {
|
|
94
|
+
return {
|
|
95
|
+
byKind: counts.byKind,
|
|
96
|
+
byLanguage: counts.byLanguage,
|
|
97
|
+
byFramework: counts.byFramework,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function buildValidationErrorResponse(issues) {
|
|
101
|
+
return {
|
|
102
|
+
tool: TOOL_NAME,
|
|
103
|
+
status: "error",
|
|
104
|
+
summary: "Request validation failed.",
|
|
105
|
+
data: { totalCount: 0, returnedCount: 0, counts: { byKind: {}, byLanguage: {}, byFramework: {} }, items: [] },
|
|
106
|
+
errors: [
|
|
107
|
+
{
|
|
108
|
+
code: "INVALID_INPUT",
|
|
109
|
+
message: "One or more input fields are invalid.",
|
|
110
|
+
retryable: false,
|
|
111
|
+
suggestion: "Correct the invalid fields and try again.",
|
|
112
|
+
details: { issues },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
meta: createResponseMeta({ query: {} }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function buildMappedErrorResponse(input, code, message, details, retryable) {
|
|
119
|
+
const query = { ...input };
|
|
120
|
+
if (code === "FILE_NOT_FOUND") {
|
|
121
|
+
return {
|
|
122
|
+
tool: TOOL_NAME,
|
|
123
|
+
status: "error",
|
|
124
|
+
summary: "Path not found.",
|
|
125
|
+
data: { totalCount: 0, returnedCount: 0, counts: { byKind: {}, byLanguage: {}, byFramework: {} }, items: [] },
|
|
126
|
+
errors: [
|
|
127
|
+
{
|
|
128
|
+
code,
|
|
129
|
+
message,
|
|
130
|
+
retryable,
|
|
131
|
+
suggestion: "Provide an existing file or directory path inside the workspace root.",
|
|
132
|
+
details,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
meta: createResponseMeta({ query }),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (code === "PATH_OUTSIDE_WORKSPACE") {
|
|
139
|
+
return {
|
|
140
|
+
tool: TOOL_NAME,
|
|
141
|
+
status: "error",
|
|
142
|
+
summary: "Path validation failed.",
|
|
143
|
+
data: { totalCount: 0, returnedCount: 0, counts: { byKind: {}, byLanguage: {}, byFramework: {} }, items: [] },
|
|
144
|
+
errors: [
|
|
145
|
+
{
|
|
146
|
+
code,
|
|
147
|
+
message,
|
|
148
|
+
retryable,
|
|
149
|
+
suggestion: "Use a path inside the workspace root or omit the path filter.",
|
|
150
|
+
details,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
meta: createResponseMeta({ query }),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (code === "UNSUPPORTED_CAPABILITY") {
|
|
157
|
+
return {
|
|
158
|
+
tool: TOOL_NAME,
|
|
159
|
+
status: "error",
|
|
160
|
+
summary: "Endpoint analysis failed.",
|
|
161
|
+
data: { totalCount: 0, returnedCount: 0, counts: { byKind: {}, byLanguage: {}, byFramework: {} }, items: [] },
|
|
162
|
+
errors: [
|
|
163
|
+
{
|
|
164
|
+
code: "BACKEND_EXECUTION_FAILED",
|
|
165
|
+
message,
|
|
166
|
+
retryable,
|
|
167
|
+
suggestion: "Verify the engine supports workspace.list_endpoints and retry.",
|
|
168
|
+
details,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
meta: createResponseMeta({ query }),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
tool: TOOL_NAME,
|
|
176
|
+
status: "error",
|
|
177
|
+
summary: "Endpoint analysis failed.",
|
|
178
|
+
data: { totalCount: 0, returnedCount: 0, counts: { byKind: {}, byLanguage: {}, byFramework: {} }, items: [] },
|
|
179
|
+
errors: [
|
|
180
|
+
{
|
|
181
|
+
code: code === "BACKEND_EXECUTION_FAILED" ? code : "BACKEND_EXECUTION_FAILED",
|
|
182
|
+
message,
|
|
183
|
+
retryable,
|
|
184
|
+
details,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
meta: createResponseMeta({ query }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function buildEngineFailureResponse(input, error) {
|
|
191
|
+
return buildMappedErrorResponse(input, "BACKEND_EXECUTION_FAILED", error instanceof Error ? error.message : String(error), {}, false);
|
|
192
|
+
}
|
|
193
|
+
function resolveEffectiveLanguage(language, framework) {
|
|
194
|
+
if (language) {
|
|
195
|
+
return language;
|
|
196
|
+
}
|
|
197
|
+
if (framework === "react-router") {
|
|
198
|
+
return "typescript";
|
|
199
|
+
}
|
|
200
|
+
if (framework === "spring") {
|
|
201
|
+
return "java";
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
function resolveAnalyzerLanguage(language, framework) {
|
|
206
|
+
const effective = resolveEffectiveLanguage(language, framework);
|
|
207
|
+
if (effective === "java") {
|
|
208
|
+
return "java";
|
|
209
|
+
}
|
|
210
|
+
if (effective === "python") {
|
|
211
|
+
return "python";
|
|
212
|
+
}
|
|
213
|
+
if (effective === "rust") {
|
|
214
|
+
return "rust";
|
|
215
|
+
}
|
|
216
|
+
if (effective === "typescript" || effective === "javascript") {
|
|
217
|
+
return "typescript";
|
|
218
|
+
}
|
|
219
|
+
return "auto";
|
|
220
|
+
}
|
|
221
|
+
function buildSummary(count, truncated, kind) {
|
|
222
|
+
const kindLabel = kind === "any" ? "endpoints" : `${kind} endpoints`;
|
|
223
|
+
if (count === 0) {
|
|
224
|
+
return `No${kindLabel} found.`;
|
|
225
|
+
}
|
|
226
|
+
if (truncated) {
|
|
227
|
+
return `Found ${count} ${kindLabel} and returned a truncated subset.`;
|
|
228
|
+
}
|
|
229
|
+
if (count === 1) {
|
|
230
|
+
return `Found 1 ${kindLabel.replace(/s$/, "")}.`;
|
|
231
|
+
}
|
|
232
|
+
return `Found ${count} ${kindLabel}.`;
|
|
233
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SearchTextData, type SearchTextInput } from "../contracts/public/code.ts";
|
|
2
|
+
import { type ResponseEnvelope } from "../contracts/public/common.ts";
|
|
3
|
+
import type { EngineClient } from "../engine/rustEngineClient.ts";
|
|
4
|
+
export interface SearchTextService {
|
|
5
|
+
execute(input: SearchTextInput): Promise<ResponseEnvelope<SearchTextData>>;
|
|
6
|
+
validateAndExecute(payload: Record<string, unknown>): Promise<ResponseEnvelope<SearchTextData>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createSearchTextService(options: {
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
engineClient: EngineClient;
|
|
11
|
+
}): SearchTextService;
|