agent-dir 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -5
- package/dist/bin/agent-dir.js +262 -12
- package/dist/bin/agent-dir.js.map +1 -1
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/auth.js +10 -2
- package/dist/src/auth.js.map +1 -1
- package/dist/src/codegraph.d.ts +33 -0
- package/dist/src/codegraph.d.ts.map +1 -0
- package/dist/src/codegraph.js +265 -0
- package/dist/src/codegraph.js.map +1 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +14 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/logging.d.ts +2 -0
- package/dist/src/logging.d.ts.map +1 -1
- package/dist/src/logging.js +36 -10
- package/dist/src/logging.js.map +1 -1
- package/dist/src/mcp.d.ts +4 -1
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +1021 -98
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/server.d.ts +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +7 -10
- package/dist/src/server.js.map +1 -1
- package/dist/src/setup.d.ts +9 -0
- package/dist/src/setup.d.ts.map +1 -0
- package/dist/src/setup.js +215 -0
- package/dist/src/setup.js.map +1 -0
- package/dist/src/telemetry/aggregate.d.ts +58 -0
- package/dist/src/telemetry/aggregate.d.ts.map +1 -0
- package/dist/src/telemetry/aggregate.js +184 -0
- package/dist/src/telemetry/aggregate.js.map +1 -0
- package/dist/src/telemetry/config.d.ts +5 -0
- package/dist/src/telemetry/config.d.ts.map +1 -0
- package/dist/src/telemetry/config.js +16 -0
- package/dist/src/telemetry/config.js.map +1 -0
- package/dist/src/telemetry/format.d.ts +3 -0
- package/dist/src/telemetry/format.d.ts.map +1 -0
- package/dist/src/telemetry/format.js +87 -0
- package/dist/src/telemetry/format.js.map +1 -0
- package/dist/src/telemetry/privacy.d.ts +4 -0
- package/dist/src/telemetry/privacy.d.ts.map +1 -0
- package/dist/src/telemetry/privacy.js +145 -0
- package/dist/src/telemetry/privacy.js.map +1 -0
- package/dist/src/telemetry/recorder.d.ts +30 -0
- package/dist/src/telemetry/recorder.d.ts.map +1 -0
- package/dist/src/telemetry/recorder.js +98 -0
- package/dist/src/telemetry/recorder.js.map +1 -0
- package/dist/src/telemetry/types.d.ts +74 -0
- package/dist/src/telemetry/types.d.ts.map +1 -0
- package/dist/src/telemetry/types.js +2 -0
- package/dist/src/telemetry/types.js.map +1 -0
- package/dist/src/tools/code.d.ts +7 -7
- package/dist/src/tools/code.d.ts.map +1 -1
- package/dist/src/tools/code.js +49 -16
- package/dist/src/tools/code.js.map +1 -1
- package/dist/src/tools/commands.d.ts +2 -1
- package/dist/src/tools/commands.d.ts.map +1 -1
- package/dist/src/tools/commands.js +10 -1
- package/dist/src/tools/commands.js.map +1 -1
- package/dist/src/tools/files.d.ts +19 -0
- package/dist/src/tools/files.d.ts.map +1 -1
- package/dist/src/tools/files.js +119 -0
- package/dist/src/tools/files.js.map +1 -1
- package/dist/src/types.d.ts +7 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +5 -3
package/dist/src/mcp.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { CodeGraphIntegration } from "./codegraph.js";
|
|
4
|
+
import { TelemetryRecorder } from "./telemetry/recorder.js";
|
|
2
5
|
import { findDefinition, findExports, findFiles, findImports, findReferences, findSymbol, searchCode, searchFiles, } from "./tools/code.js";
|
|
3
6
|
import { runCommand } from "./tools/commands.js";
|
|
4
7
|
import { diagnostics } from "./tools/diagnostics.js";
|
|
5
|
-
import { deleteFile, listDirs, listFiles, patchFiles, readFile, readRange, writeFile, } from "./tools/files.js";
|
|
8
|
+
import { applyFileChanges, deleteFile, listDirs, listFiles, patchFiles, readFile, readRange, writeFile, } from "./tools/files.js";
|
|
6
9
|
import { gitCommit, gitDiff, gitLog, gitPush, gitRestore, gitStage, gitStatus, gitUnstage, } from "./tools/git.js";
|
|
7
10
|
import { fileInfo, packageInfo, projectOverview } from "./tools/metadata.js";
|
|
8
11
|
import { runNpm } from "./tools/npm.js";
|
|
@@ -16,6 +19,7 @@ const SERVER_INFO_META = { "io.modelcontextprotocol/serverInfo": SERVER_INFO };
|
|
|
16
19
|
const PROTOCOL_META = "io.modelcontextprotocol/protocolVersion";
|
|
17
20
|
const CLIENT_CAPABILITIES_META = "io.modelcontextprotocol/clientCapabilities";
|
|
18
21
|
const PAGE_SIZE = 50;
|
|
22
|
+
const DEFAULT_OUTPUT_BYTES = 32_000;
|
|
19
23
|
const INSTRUCTIONS_URI = "agent-dir://instructions";
|
|
20
24
|
const CAPABILITIES_URI = "agent-dir://capabilities";
|
|
21
25
|
const SUBSCRIPTION_ID_META = "io.modelcontextprotocol/subscriptionId";
|
|
@@ -43,9 +47,18 @@ const commandResultSchema = objectOutput({
|
|
|
43
47
|
stdout: stringSchema,
|
|
44
48
|
stderr: stringSchema,
|
|
45
49
|
}, ["command", "exitCode", "stdout", "stderr"]);
|
|
50
|
+
const codeGraphTool = tool("codegraph_explore", "Explore the current project through its optional CodeGraph MCP index. Returns CodeGraph native structured tool results for structural, flow, and symbol questions.", {
|
|
51
|
+
query: { type: "string", minLength: 1 },
|
|
52
|
+
maxFiles: { type: "integer", minimum: 1, maximum: 100 },
|
|
53
|
+
}, { type: "object" }, ["query"]);
|
|
46
54
|
const baseTools = [
|
|
47
|
-
tool("list_files", "List all visible project files and directories recursively up to the server's discovery depth. Prefer project_overview, find_files, search_code, or list_dirs for targeted discovery; use this only when broad recursive discovery is actually needed.", {
|
|
48
|
-
|
|
55
|
+
tool("list_files", "List all visible project files and directories recursively up to the server's discovery depth. Prefer project_overview, find_files, search_code, or list_dirs for targeted discovery; use this only when broad recursive discovery is actually needed.", {
|
|
56
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
57
|
+
}, arrayOutput(fileEntrySchema)),
|
|
58
|
+
tool("list_dirs", "List files and directories directly inside one or more project directories.", {
|
|
59
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
60
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
61
|
+
}, arrayOutput(objectOutput({ path: stringSchema, entries: arrayOutput(fileEntrySchema) }, [
|
|
49
62
|
"path",
|
|
50
63
|
"entries",
|
|
51
64
|
])), ["paths"]),
|
|
@@ -53,14 +66,50 @@ const baseTools = [
|
|
|
53
66
|
path: pathSchema,
|
|
54
67
|
startLine: { type: "integer", minimum: 1 },
|
|
55
68
|
endLine: { type: "integer", minimum: 1 },
|
|
69
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
56
70
|
}, objectOutput({
|
|
57
71
|
path: stringSchema,
|
|
58
72
|
startLine: { type: "integer" },
|
|
59
73
|
endLine: { type: "integer" },
|
|
60
74
|
content: stringSchema,
|
|
61
75
|
}, ["path", "startLine", "endLine", "content"]), ["path", "startLine", "endLine"]),
|
|
62
|
-
tool("read_files", "Read one or more UTF-8 project files in one call. Prefer read_range when only a bounded section is needed; batch related files here to reduce round trips.", {
|
|
76
|
+
tool("read_files", "Read one or more UTF-8 project files in one call. Prefer read_range when only a bounded section is needed; batch related files here to reduce round trips.", {
|
|
77
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
78
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
79
|
+
}, arrayOutput(objectOutput({ path: stringSchema, content: stringSchema }, ["path", "content"])), ["paths"]),
|
|
80
|
+
tool("apply_changes", "Apply multiple file writes, exact patches, and deletes as one preflighted transaction. Use this for related multi-file edits when partial changes would be undesirable; supports dry-run previews.", {
|
|
81
|
+
dryRun: { type: "boolean", default: false },
|
|
82
|
+
changes: {
|
|
83
|
+
type: "array",
|
|
84
|
+
minItems: 1,
|
|
85
|
+
items: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
kind: { type: "string", enum: ["write", "patch", "delete"] },
|
|
89
|
+
path: pathSchema,
|
|
90
|
+
content: stringSchema,
|
|
91
|
+
patches: {
|
|
92
|
+
type: "array",
|
|
93
|
+
items: objectOutput({
|
|
94
|
+
search: stringSchema,
|
|
95
|
+
replace: stringSchema,
|
|
96
|
+
count: { type: "integer", minimum: 1 },
|
|
97
|
+
}, ["search", "replace"]),
|
|
98
|
+
minItems: 1,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: ["kind", "path"],
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}, arrayOutput(objectOutput({
|
|
106
|
+
kind: { type: "string", enum: ["write", "patch", "delete"] },
|
|
107
|
+
path: stringSchema,
|
|
108
|
+
changed: { type: "boolean" },
|
|
109
|
+
dryRun: { type: "boolean" },
|
|
110
|
+
}, ["kind", "path", "changed"])), ["changes"]),
|
|
63
111
|
tool("write_files", "Create or completely replace one or more UTF-8 project files.", {
|
|
112
|
+
dryRun: { type: "boolean", default: false },
|
|
64
113
|
files: {
|
|
65
114
|
type: "array",
|
|
66
115
|
items: objectOutput({ path: pathSchema, content: stringSchema }, ["path", "content"]),
|
|
@@ -68,6 +117,7 @@ const baseTools = [
|
|
|
68
117
|
},
|
|
69
118
|
}, arrayOutput(objectOutput({ path: stringSchema, written: { type: "boolean" } }, ["path", "written"])), ["files"]),
|
|
70
119
|
tool("patch_files", "Apply exact text replacements to one or more UTF-8 files. Prefer this for targeted edits instead of rewriting whole files; batch related patches in one call.", {
|
|
120
|
+
dryRun: { type: "boolean", default: false },
|
|
71
121
|
files: {
|
|
72
122
|
type: "array",
|
|
73
123
|
items: objectOutput({
|
|
@@ -84,29 +134,37 @@ const baseTools = [
|
|
|
84
134
|
}, ["path", "patches"]),
|
|
85
135
|
minItems: 1,
|
|
86
136
|
},
|
|
137
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
87
138
|
}, arrayOutput(objectOutput({ path: stringSchema, applied: { type: "integer" }, content: stringSchema }, [
|
|
88
139
|
"path",
|
|
89
140
|
"applied",
|
|
90
141
|
"content",
|
|
91
142
|
])), ["files"]),
|
|
92
|
-
tool("delete_files", "Delete one or more project files.", {
|
|
143
|
+
tool("delete_files", "Delete one or more project files.", {
|
|
144
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
145
|
+
dryRun: { type: "boolean", default: false },
|
|
146
|
+
}, arrayOutput(objectOutput({ path: stringSchema, deleted: { type: "boolean" } }, ["path", "deleted"])), ["paths"]),
|
|
93
147
|
tool("search_files", "Search text across visible project files. Prefer this over broad file reads; use search_code for source-only searches and keep maxResults tight.", {
|
|
94
148
|
query: stringSchema,
|
|
95
149
|
regex: { type: "boolean", default: false },
|
|
96
150
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
151
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
97
152
|
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
98
153
|
tool("find_files", "Find project files by a simple glob pattern using * and ?. Prefer this over list_files when you know the filename pattern.", {
|
|
99
154
|
pattern: stringSchema,
|
|
100
155
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
156
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
101
157
|
}, arrayOutput(stringSchema), ["pattern"]),
|
|
102
158
|
tool("search_code", "Search source-like project files for text or a regular expression. Prefer this over reading files broadly; start with a narrow query and expand only if needed.", {
|
|
103
159
|
query: stringSchema,
|
|
104
160
|
regex: { type: "boolean", default: false },
|
|
105
161
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
162
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
106
163
|
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
107
164
|
tool("find_symbol", "Find likely symbol definitions across source files. Prefer this over reading whole modules when locating an implementation.", {
|
|
108
165
|
symbol: stringSchema,
|
|
109
166
|
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
167
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
110
168
|
}, arrayOutput(objectOutput({
|
|
111
169
|
...searchMatchSchema.properties,
|
|
112
170
|
symbol: stringSchema,
|
|
@@ -115,6 +173,7 @@ const baseTools = [
|
|
|
115
173
|
tool("find_definition", "Find likely definitions of a symbol. Use this before reading a large file when you need one implementation.", {
|
|
116
174
|
symbol: stringSchema,
|
|
117
175
|
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
176
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
118
177
|
}, arrayOutput(objectOutput({
|
|
119
178
|
...searchMatchSchema.properties,
|
|
120
179
|
symbol: stringSchema,
|
|
@@ -123,25 +182,101 @@ const baseTools = [
|
|
|
123
182
|
tool("find_references", "Find source-code references to a symbol. Use this instead of searching entire files when tracing usage of a known symbol.", {
|
|
124
183
|
symbol: stringSchema,
|
|
125
184
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
185
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
126
186
|
}, arrayOutput(searchMatchSchema), ["symbol"]),
|
|
127
|
-
tool("find_imports", "Find import and require statements in source files.", {
|
|
187
|
+
tool("find_imports", "Find import and require statements in source files.", {
|
|
188
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
189
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
190
|
+
}, arrayOutput(objectOutput({
|
|
128
191
|
...searchMatchSchema.properties,
|
|
129
192
|
module: stringSchema,
|
|
130
193
|
kind: { type: "string", enum: ["import", "require"] },
|
|
131
194
|
}, ["path", "line", "column", "text", "module", "kind"]))),
|
|
132
|
-
tool("find_exports", "Find exported declarations in source files.", {
|
|
195
|
+
tool("find_exports", "Find exported declarations in source files.", {
|
|
196
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
197
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
198
|
+
}, arrayOutput(objectOutput({
|
|
133
199
|
...searchMatchSchema.properties,
|
|
134
200
|
symbol: stringSchema,
|
|
135
201
|
kind: { type: "string", const: "export" },
|
|
136
202
|
}, ["path", "line", "column", "text", "symbol", "kind"]))),
|
|
137
|
-
tool("git_status", "Show Git working-tree status and branch information. Use this as the lightweight first Git check; do not run a full diff unless changes need inspection.", {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
tool("
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
tool("
|
|
203
|
+
tool("git_status", "Show Git working-tree status and branch information. Use this as the lightweight first Git check; do not run a full diff unless changes need inspection.", {
|
|
204
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
205
|
+
}, commandResultSchema),
|
|
206
|
+
tool("git_stage", "Stage one or more Git paths.", {
|
|
207
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
208
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
209
|
+
}, commandResultSchema, ["paths"]),
|
|
210
|
+
tool("git_unstage", "Unstage one or more Git paths without changing their working-tree contents.", {
|
|
211
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
212
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
213
|
+
}, commandResultSchema, ["paths"]),
|
|
214
|
+
tool("git_commit", "Create a Git commit with the currently staged changes.", {
|
|
215
|
+
message: stringSchema,
|
|
216
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
217
|
+
}, commandResultSchema, ["message"]),
|
|
218
|
+
tool("git_restore", "Restore one or more Git paths from the index, discarding unstaged working-tree changes.", {
|
|
219
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
220
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
221
|
+
}, commandResultSchema, ["paths"]),
|
|
222
|
+
tool("git_push", "Push the current Git branch to a remote.", {
|
|
223
|
+
remote: stringSchema,
|
|
224
|
+
branch: stringSchema,
|
|
225
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
226
|
+
}, commandResultSchema),
|
|
227
|
+
tool("git_diff", "Show a read-only Git diff, optionally staged or limited to a path. Prefer path-scoped or staged diffs over dumping the entire repository diff.", {
|
|
228
|
+
staged: { type: "boolean", default: false },
|
|
229
|
+
path: pathSchema,
|
|
230
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
231
|
+
}, commandResultSchema),
|
|
232
|
+
tool("git_log", "Show recent Git commits in a compact, read-only form. Keep the limit small and scope by path when history context is local.", {
|
|
233
|
+
limit: { type: "integer", minimum: 1, maximum: 200, default: 20 },
|
|
234
|
+
path: pathSchema,
|
|
235
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
236
|
+
}, commandResultSchema),
|
|
237
|
+
tool("locate", "Locate project code or files in one call. Use kind auto, code, text, or file.", {
|
|
238
|
+
query: stringSchema,
|
|
239
|
+
kind: { type: "string", enum: ["auto", "code", "text", "file"], default: "auto" },
|
|
240
|
+
regex: { type: "boolean", default: false },
|
|
241
|
+
maxResults: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
242
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
243
|
+
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
244
|
+
tool("inspect_symbol", "Inspect a known symbol in one call, returning its definition, bounded source context, and optionally references.", {
|
|
245
|
+
symbol: stringSchema,
|
|
246
|
+
includeReferences: { type: "boolean", default: false },
|
|
247
|
+
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
248
|
+
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
249
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
250
|
+
}, { type: "object", additionalProperties: true }, ["symbol"]),
|
|
251
|
+
tool("inspect", "Inspect a project target in one call. Use kind auto, file, symbol, or code; returns bounded context and optional symbol references.", {
|
|
252
|
+
target: stringSchema,
|
|
253
|
+
kind: { type: "string", enum: ["auto", "file", "symbol", "code"], default: "auto" },
|
|
254
|
+
includeReferences: { type: "boolean", default: false },
|
|
255
|
+
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
256
|
+
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
257
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
258
|
+
}, { type: "object", additionalProperties: true }, ["target"]),
|
|
259
|
+
tool("project_context", "Return a compact LLM-oriented project snapshot combining project overview, package metadata, and active execution policy. Prefer this over separate orientation calls.", {
|
|
260
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
261
|
+
}, { type: "object", additionalProperties: true }),
|
|
262
|
+
tool("read_relevant", "Locate relevant source matches for a query and return bounded source context in one call. Prefer this when you know what you need but not the exact lines.", {
|
|
263
|
+
query: stringSchema,
|
|
264
|
+
regex: { type: "boolean", default: false },
|
|
265
|
+
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
266
|
+
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
267
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
268
|
+
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
269
|
+
tool("validate", "Run a unified project validation pass: lightweight diagnostics first, then explicitly requested allowed npm scripts. Use diagnostics for cheap checks and scripts only when executable validation is needed.", {
|
|
270
|
+
level: { type: "string", enum: ["diagnostics", "scripts", "full"], default: "diagnostics" },
|
|
271
|
+
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
272
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
273
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
274
|
+
}, { type: "object", additionalProperties: true }),
|
|
275
|
+
tool("git_changes", "Return a compact Git working-tree snapshot combining status and an optional diff. Prefer this over separate git_status and git_diff calls when reviewing changes.", {
|
|
276
|
+
includeDiff: { type: "boolean", default: false },
|
|
277
|
+
path: pathSchema,
|
|
278
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
279
|
+
}, { type: "object", additionalProperties: true }),
|
|
145
280
|
tool("project_overview", "Summarize project structure, languages, package managers, project metadata files, and Git state in one lightweight call. Prefer this before broad discovery.", {}, objectOutput({
|
|
146
281
|
root: stringSchema,
|
|
147
282
|
files: { type: "integer" },
|
|
@@ -157,20 +292,27 @@ const baseTools = [
|
|
|
157
292
|
commands: { type: "array", items: stringSchema },
|
|
158
293
|
}, ["npmScripts", "commands"])),
|
|
159
294
|
tool("file_info", "Return filesystem metadata for a project path.", { path: pathSchema }, { type: "object", additionalProperties: true }, ["path"]),
|
|
160
|
-
tool("diagnostics", "Run lightweight project-independent diagnostics without invoking tests, linters, or typecheckers. Prefer this before expensive validation when checking basic file/JSON/conflict issues.", {
|
|
295
|
+
tool("diagnostics", "Run lightweight project-independent diagnostics without invoking tests, linters, or typecheckers. Prefer this before expensive validation when checking basic file/JSON/conflict issues.", {
|
|
296
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
297
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
298
|
+
}, arrayOutput(objectOutput({
|
|
161
299
|
severity: { type: "string", enum: ["error", "warning", "info"] },
|
|
162
300
|
code: stringSchema,
|
|
163
301
|
path: stringSchema,
|
|
164
302
|
line: { type: "integer" },
|
|
165
303
|
message: stringSchema,
|
|
166
304
|
}, ["severity", "code", "message"]))),
|
|
167
|
-
tool("run_npm_batch", "Run one or more explicitly allowed npm scripts sequentially. Prefer the narrowest relevant script; batch independent scripts in one call and avoid running full check/test when a targeted check answers the question.", {
|
|
305
|
+
tool("run_npm_batch", "Run one or more explicitly allowed npm scripts sequentially. Prefer the narrowest relevant script; batch independent scripts in one call and avoid running full check/test when a targeted check answers the question.", {
|
|
306
|
+
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
307
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
308
|
+
}, arrayOutput(commandResultSchema), ["scripts"]),
|
|
168
309
|
tool("run_command_batch", "Run one or more explicitly allowed commands sequentially without a shell. Prefer dedicated Agent Dir tools over generic commands because they return structured, bounded results with less token overhead.", {
|
|
169
310
|
commands: {
|
|
170
311
|
type: "array",
|
|
171
312
|
items: objectOutput({ command: stringSchema, args: { type: "array", items: stringSchema } }, ["command"]),
|
|
172
313
|
minItems: 1,
|
|
173
314
|
},
|
|
315
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
174
316
|
}, arrayOutput(commandResultSchema), ["commands"]),
|
|
175
317
|
];
|
|
176
318
|
function tool(name, description, properties, outputSchema, required = []) {
|
|
@@ -187,9 +329,28 @@ function tool(name, description, properties, outputSchema, required = []) {
|
|
|
187
329
|
outputSchema,
|
|
188
330
|
};
|
|
189
331
|
}
|
|
190
|
-
export function createMcpHandler(root, commandConfig = {}) {
|
|
332
|
+
export function createMcpHandler(root, commandConfig = {}, telemetry = { level: "none" }) {
|
|
333
|
+
const codeGraph = new CodeGraphIntegration(root);
|
|
334
|
+
const telemetryRecorder = new TelemetryRecorder({
|
|
335
|
+
level: telemetry.level,
|
|
336
|
+
persist: telemetry.persist ?? false,
|
|
337
|
+
...(telemetry.configId ? { configId: telemetry.configId } : {}),
|
|
338
|
+
sessionId: telemetry.sessionId ?? randomUUID(),
|
|
339
|
+
});
|
|
340
|
+
let sessionStartedAt;
|
|
341
|
+
let sessionLastActivityAt;
|
|
342
|
+
let sessionActiveRequestDurationMs = 0;
|
|
343
|
+
let sessionRequestCount = 0;
|
|
344
|
+
let sessionToolCallCount = 0;
|
|
345
|
+
let sessionCommandCallCount = 0;
|
|
346
|
+
let sessionRecorded = false;
|
|
347
|
+
const telemetryProjectPromise = telemetry.level === "detailed"
|
|
348
|
+
? buildTelemetryProjectContext(root, codeGraph).catch(() => undefined)
|
|
349
|
+
: undefined;
|
|
191
350
|
const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
|
|
192
351
|
const allowedCommands = commandConfig.commands ?? [];
|
|
352
|
+
const blacklistedCommands = commandConfig.blacklistedCommands ?? [];
|
|
353
|
+
const gitEnabled = commandConfig.git ?? allowedCommands.includes("git");
|
|
193
354
|
const subscriptions = new Set();
|
|
194
355
|
const notify = (event) => {
|
|
195
356
|
for (const subscription of subscriptions) {
|
|
@@ -220,23 +381,65 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
220
381
|
}
|
|
221
382
|
};
|
|
222
383
|
const handler = async (request) => {
|
|
384
|
+
const requestStartedAt = performance.now();
|
|
385
|
+
if (sessionStartedAt === undefined)
|
|
386
|
+
sessionStartedAt = requestStartedAt;
|
|
387
|
+
sessionRequestCount += 1;
|
|
388
|
+
const telemetryProject = telemetryProjectPromise ? await telemetryProjectPromise : undefined;
|
|
223
389
|
let message;
|
|
390
|
+
let mcpSuccess = false;
|
|
391
|
+
let runtime;
|
|
392
|
+
let telemetryRequestErrorCategory;
|
|
393
|
+
const recordMcpRequest = (method = message?.method ?? "unknown") => {
|
|
394
|
+
telemetryRecorder.record({
|
|
395
|
+
event: "mcp_request",
|
|
396
|
+
method,
|
|
397
|
+
success: mcpSuccess,
|
|
398
|
+
durationMs: performance.now() - requestStartedAt,
|
|
399
|
+
...(telemetryRequestErrorCategory
|
|
400
|
+
? { errorCategory: telemetryRequestErrorCategory }
|
|
401
|
+
: {}),
|
|
402
|
+
}, runtime, telemetryProject);
|
|
403
|
+
};
|
|
224
404
|
try {
|
|
225
|
-
|
|
405
|
+
const parsed = await request.json();
|
|
406
|
+
if (!isRecord(parsed)) {
|
|
407
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
408
|
+
recordMcpRequest("unknown");
|
|
409
|
+
return jsonRpcError(null, -32600, "Invalid Request", 400);
|
|
410
|
+
}
|
|
411
|
+
message = parsed;
|
|
226
412
|
}
|
|
227
413
|
catch {
|
|
414
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
415
|
+
recordMcpRequest("unknown");
|
|
228
416
|
return jsonRpcError(null, -32700, "Parse error", 400);
|
|
229
417
|
}
|
|
230
|
-
if (message.jsonrpc !== "2.0" || typeof message.method !== "string")
|
|
418
|
+
if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
419
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
420
|
+
recordMcpRequest(typeof message?.method === "string" ? message.method : "unknown");
|
|
231
421
|
return jsonRpcError(message.id ?? null, -32600, "Invalid Request", 400);
|
|
422
|
+
}
|
|
232
423
|
const hasId = Object.hasOwn(message, "id");
|
|
233
|
-
if (hasId && !isValidRequestId(message.id))
|
|
424
|
+
if (hasId && !isValidRequestId(message.id)) {
|
|
425
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
426
|
+
recordMcpRequest(message.method);
|
|
234
427
|
return jsonRpcError(null, -32600, "Invalid Request", 400);
|
|
235
|
-
|
|
428
|
+
}
|
|
429
|
+
if (!hasId) {
|
|
430
|
+
mcpSuccess = message.method.startsWith("notifications/");
|
|
431
|
+
if (!mcpSuccess)
|
|
432
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
433
|
+
recordMcpRequest();
|
|
236
434
|
return new Response(null, { status: 202 });
|
|
237
|
-
|
|
435
|
+
}
|
|
436
|
+
if (message.params !== undefined && !isRecord(message.params)) {
|
|
437
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
438
|
+
recordMcpRequest();
|
|
238
439
|
return jsonRpcError(message.id, -32602, "Invalid params.", 400);
|
|
440
|
+
}
|
|
239
441
|
const params = message.params ?? {};
|
|
442
|
+
runtime = telemetryRuntime(params);
|
|
240
443
|
const requestedProtocolVersion = typeof params.protocolVersion === "string" ? params.protocolVersion : undefined;
|
|
241
444
|
const legacyInitialize = message.method === "initialize";
|
|
242
445
|
const legacyRequest = !legacyInitialize &&
|
|
@@ -244,15 +447,26 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
244
447
|
!isRecord(params._meta);
|
|
245
448
|
if (legacyInitialize) {
|
|
246
449
|
const protocolHeader = request.headers.get("mcp-protocol-version");
|
|
247
|
-
if (protocolHeader !== null && protocolHeader !== LEGACY_PROTOCOL_VERSION)
|
|
450
|
+
if (protocolHeader !== null && protocolHeader !== LEGACY_PROTOCOL_VERSION) {
|
|
451
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
452
|
+
recordMcpRequest();
|
|
248
453
|
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version does not match the initialize protocol version.", 400);
|
|
249
|
-
|
|
454
|
+
}
|
|
455
|
+
if (requestedProtocolVersion !== LEGACY_PROTOCOL_VERSION) {
|
|
456
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
457
|
+
recordMcpRequest();
|
|
250
458
|
return jsonRpcError(message.id ?? null, -32602, "Unsupported protocol version.", 400, {
|
|
251
459
|
supported: [LEGACY_PROTOCOL_VERSION],
|
|
252
460
|
requested: requestedProtocolVersion,
|
|
253
461
|
});
|
|
254
|
-
|
|
462
|
+
}
|
|
463
|
+
if (!isRecord(params.capabilities) || !isRecord(params.clientInfo)) {
|
|
464
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
465
|
+
recordMcpRequest();
|
|
255
466
|
return jsonRpcError(message.id ?? null, -32602, "Invalid initialize params.", 400);
|
|
467
|
+
}
|
|
468
|
+
mcpSuccess = true;
|
|
469
|
+
recordMcpRequest();
|
|
256
470
|
return json({
|
|
257
471
|
jsonrpc: "2.0",
|
|
258
472
|
id: message.id,
|
|
@@ -260,30 +474,46 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
260
474
|
protocolVersion: LEGACY_PROTOCOL_VERSION,
|
|
261
475
|
capabilities: capabilities(),
|
|
262
476
|
serverInfo: SERVER_INFO,
|
|
263
|
-
instructions: buildAgentInstructions(npmAllowed, allowedCommands),
|
|
477
|
+
instructions: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
264
478
|
},
|
|
265
479
|
});
|
|
266
480
|
}
|
|
267
481
|
if (legacyRequest) {
|
|
268
|
-
if (request.headers.get("mcp-protocol-version") !== LEGACY_PROTOCOL_VERSION)
|
|
482
|
+
if (request.headers.get("mcp-protocol-version") !== LEGACY_PROTOCOL_VERSION) {
|
|
483
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
484
|
+
recordMcpRequest();
|
|
269
485
|
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version must be 2025-11-25 for legacy requests.", 400);
|
|
486
|
+
}
|
|
270
487
|
}
|
|
271
|
-
else if (!isRecord(params._meta))
|
|
488
|
+
else if (!isRecord(params._meta)) {
|
|
489
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
490
|
+
recordMcpRequest();
|
|
272
491
|
return jsonRpcError(message.id ?? null, -32602, "Missing required request metadata.", 400);
|
|
492
|
+
}
|
|
273
493
|
const meta = isRecord(params._meta) ? params._meta : {};
|
|
274
494
|
const version = meta[PROTOCOL_META];
|
|
275
|
-
if (!legacyRequest && typeof version !== "string")
|
|
495
|
+
if (!legacyRequest && typeof version !== "string") {
|
|
496
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
497
|
+
recordMcpRequest();
|
|
276
498
|
return jsonRpcError(message.id ?? null, -32602, "Missing required protocol version metadata.", 400);
|
|
277
|
-
|
|
499
|
+
}
|
|
500
|
+
if (!legacyRequest && version !== PROTOCOL_VERSION) {
|
|
501
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
502
|
+
recordMcpRequest();
|
|
278
503
|
return jsonRpcError(message.id ?? null, -32022, "Unsupported protocol version.", 400, {
|
|
279
504
|
supported: [PROTOCOL_VERSION],
|
|
280
505
|
requested: version,
|
|
281
506
|
});
|
|
282
|
-
|
|
507
|
+
}
|
|
508
|
+
if (!legacyRequest && !isRecord(meta[CLIENT_CAPABILITIES_META])) {
|
|
509
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
510
|
+
recordMcpRequest();
|
|
283
511
|
return jsonRpcError(message.id ?? null, -32602, "Missing required client capabilities metadata.", 400);
|
|
512
|
+
}
|
|
284
513
|
try {
|
|
285
514
|
switch (message.method) {
|
|
286
515
|
case "server/discover":
|
|
516
|
+
mcpSuccess = true;
|
|
287
517
|
return json({
|
|
288
518
|
jsonrpc: "2.0",
|
|
289
519
|
id: message.id,
|
|
@@ -292,13 +522,15 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
292
522
|
supportedVersions: [PROTOCOL_VERSION],
|
|
293
523
|
capabilities: capabilities(),
|
|
294
524
|
_meta: SERVER_INFO_META,
|
|
295
|
-
instructions: buildAgentInstructions(npmAllowed, allowedCommands),
|
|
525
|
+
instructions: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
526
|
+
codeGraph: await codeGraph.capability(),
|
|
296
527
|
ttlMs: 0,
|
|
297
528
|
cacheScope: "private",
|
|
298
529
|
},
|
|
299
530
|
});
|
|
300
531
|
case "tools/list": {
|
|
301
|
-
const page = paginate(
|
|
532
|
+
const page = paginate(await availableTools(codeGraph, gitEnabled), params.cursor);
|
|
533
|
+
mcpSuccess = true;
|
|
302
534
|
return json({
|
|
303
535
|
jsonrpc: "2.0",
|
|
304
536
|
id: message.id,
|
|
@@ -313,23 +545,86 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
313
545
|
});
|
|
314
546
|
}
|
|
315
547
|
case "tools/call": {
|
|
316
|
-
if (typeof params.name !== "string" || params.name.length === 0)
|
|
548
|
+
if (typeof params.name !== "string" || params.name.length === 0) {
|
|
549
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
317
550
|
return jsonRpcError(message.id ?? null, -32602, "tools/call name must be a non-empty string.", 400);
|
|
551
|
+
}
|
|
318
552
|
const toolName = params.name;
|
|
319
|
-
const toolDefinition =
|
|
553
|
+
const toolDefinition = (await availableTools(codeGraph, gitEnabled)).find((item) => item.name === toolName);
|
|
320
554
|
if (!toolDefinition)
|
|
321
555
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
322
|
-
if (params.arguments !== undefined && !isRecord(params.arguments))
|
|
556
|
+
if (params.arguments !== undefined && !isRecord(params.arguments)) {
|
|
557
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
323
558
|
return jsonRpcError(message.id ?? null, -32602, "tools/call arguments must be an object.", 400);
|
|
559
|
+
}
|
|
324
560
|
const argumentsValue = params.arguments ?? {};
|
|
325
561
|
const headerError = validateMcpParamHeaders(toolDefinition.inputSchema, argumentsValue, request.headers);
|
|
326
|
-
if (headerError)
|
|
562
|
+
if (headerError) {
|
|
563
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
327
564
|
return jsonRpcError(message.id ?? null, -32020, headerError, 400);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
565
|
+
}
|
|
566
|
+
sessionToolCallCount += 1;
|
|
567
|
+
const startedAt = performance.now();
|
|
568
|
+
try {
|
|
569
|
+
const result = await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, notify, request.signal, codeGraph);
|
|
570
|
+
const durationMs = performance.now() - startedAt;
|
|
571
|
+
const count = resultCount(result);
|
|
572
|
+
const runtime = telemetryRuntime(params);
|
|
573
|
+
telemetryRecorder.record({
|
|
574
|
+
event: "tool_call",
|
|
575
|
+
tool: toolName,
|
|
576
|
+
success: true,
|
|
577
|
+
durationMs,
|
|
578
|
+
inputBytes: Buffer.byteLength(JSON.stringify(argumentsValue), "utf8"),
|
|
579
|
+
outputBytes: Buffer.byteLength(JSON.stringify(result), "utf8"),
|
|
580
|
+
...(count === undefined ? {} : { resultCount: count }),
|
|
581
|
+
...toolTelemetryMetrics(toolName, result),
|
|
582
|
+
}, runtime, telemetryProject);
|
|
583
|
+
const command = commandTelemetryDescriptor(toolName);
|
|
584
|
+
if (command) {
|
|
585
|
+
sessionCommandCallCount += 1;
|
|
586
|
+
telemetryRecorder.record({
|
|
587
|
+
event: "command_call",
|
|
588
|
+
commandFamily: command.family,
|
|
589
|
+
operation: command.operation,
|
|
590
|
+
success: true,
|
|
591
|
+
durationMs,
|
|
592
|
+
outputBytes: Buffer.byteLength(JSON.stringify(result), "utf8"),
|
|
593
|
+
}, runtime, telemetryProject);
|
|
594
|
+
}
|
|
595
|
+
mcpSuccess = true;
|
|
596
|
+
return json({
|
|
597
|
+
jsonrpc: "2.0",
|
|
598
|
+
id: message.id,
|
|
599
|
+
result,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
const durationMs = performance.now() - startedAt;
|
|
604
|
+
const runtime = telemetryRuntime(params);
|
|
605
|
+
const errorCategory = classifyTelemetryError(error);
|
|
606
|
+
telemetryRecorder.record({
|
|
607
|
+
event: "tool_call",
|
|
608
|
+
tool: toolName,
|
|
609
|
+
success: false,
|
|
610
|
+
durationMs,
|
|
611
|
+
inputBytes: Buffer.byteLength(JSON.stringify(argumentsValue), "utf8"),
|
|
612
|
+
errorCategory,
|
|
613
|
+
}, runtime, telemetryProject);
|
|
614
|
+
const command = commandTelemetryDescriptor(toolName);
|
|
615
|
+
if (command) {
|
|
616
|
+
sessionCommandCallCount += 1;
|
|
617
|
+
telemetryRecorder.record({
|
|
618
|
+
event: "command_call",
|
|
619
|
+
commandFamily: command.family,
|
|
620
|
+
operation: command.operation,
|
|
621
|
+
success: false,
|
|
622
|
+
durationMs,
|
|
623
|
+
errorCategory,
|
|
624
|
+
}, runtime, telemetryProject);
|
|
625
|
+
}
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
333
628
|
}
|
|
334
629
|
case "resources/list": {
|
|
335
630
|
const resources = [
|
|
@@ -351,6 +646,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
351
646
|
})),
|
|
352
647
|
];
|
|
353
648
|
const page = paginate(resources, params.cursor);
|
|
649
|
+
mcpSuccess = true;
|
|
354
650
|
return json({
|
|
355
651
|
jsonrpc: "2.0",
|
|
356
652
|
id: message.id,
|
|
@@ -367,7 +663,8 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
367
663
|
case "resources/read": {
|
|
368
664
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
369
665
|
return jsonRpcError(message.id ?? null, -32602, "resources/read uri must be a non-empty string.", 400);
|
|
370
|
-
if (params.uri === INSTRUCTIONS_URI)
|
|
666
|
+
if (params.uri === INSTRUCTIONS_URI) {
|
|
667
|
+
mcpSuccess = true;
|
|
371
668
|
return json({
|
|
372
669
|
jsonrpc: "2.0",
|
|
373
670
|
id: message.id,
|
|
@@ -377,7 +674,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
377
674
|
{
|
|
378
675
|
uri: INSTRUCTIONS_URI,
|
|
379
676
|
mimeType: "text/markdown",
|
|
380
|
-
text: buildAgentInstructions(npmAllowed, allowedCommands),
|
|
677
|
+
text: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
381
678
|
},
|
|
382
679
|
],
|
|
383
680
|
ttlMs: 0,
|
|
@@ -385,7 +682,9 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
385
682
|
_meta: SERVER_INFO_META,
|
|
386
683
|
},
|
|
387
684
|
});
|
|
388
|
-
|
|
685
|
+
}
|
|
686
|
+
if (params.uri === CAPABILITIES_URI) {
|
|
687
|
+
mcpSuccess = true;
|
|
389
688
|
return json({
|
|
390
689
|
jsonrpc: "2.0",
|
|
391
690
|
id: message.id,
|
|
@@ -395,7 +694,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
395
694
|
{
|
|
396
695
|
uri: CAPABILITIES_URI,
|
|
397
696
|
mimeType: "application/json",
|
|
398
|
-
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands), null, 2),
|
|
697
|
+
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled), null, 2),
|
|
399
698
|
},
|
|
400
699
|
],
|
|
401
700
|
ttlMs: 0,
|
|
@@ -403,7 +702,9 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
403
702
|
_meta: SERVER_INFO_META,
|
|
404
703
|
},
|
|
405
704
|
});
|
|
705
|
+
}
|
|
406
706
|
const value = await readSkillResource(root, params.uri);
|
|
707
|
+
mcpSuccess = true;
|
|
407
708
|
return json({
|
|
408
709
|
jsonrpc: "2.0",
|
|
409
710
|
id: message.id,
|
|
@@ -421,6 +722,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
421
722
|
return jsonRpcError(message.id ?? null, -32602, "resources/directory/read uri must be a non-empty string.", 400);
|
|
422
723
|
const resources = await readSkillDirectory(root, params.uri);
|
|
423
724
|
const page = paginate(resources, params.cursor);
|
|
725
|
+
mcpSuccess = true;
|
|
424
726
|
return json({
|
|
425
727
|
jsonrpc: "2.0",
|
|
426
728
|
id: message.id,
|
|
@@ -436,6 +738,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
436
738
|
}
|
|
437
739
|
case "skills/list": {
|
|
438
740
|
const page = paginate(await listSkills(root), params.cursor);
|
|
741
|
+
mcpSuccess = true;
|
|
439
742
|
return json({
|
|
440
743
|
jsonrpc: "2.0",
|
|
441
744
|
id: message.id,
|
|
@@ -451,11 +754,13 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
451
754
|
}
|
|
452
755
|
case "subscriptions/listen": {
|
|
453
756
|
const notifications = subscriptionNotifications(params.notifications);
|
|
757
|
+
mcpSuccess = true;
|
|
454
758
|
return subscriptionResponse(message.id, notifications, subscriptions);
|
|
455
759
|
}
|
|
456
760
|
case "skills/get":
|
|
457
761
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
458
762
|
return jsonRpcError(message.id ?? null, -32602, "skills/get uri must be a non-empty string.", 400);
|
|
763
|
+
mcpSuccess = true;
|
|
459
764
|
return json({
|
|
460
765
|
jsonrpc: "2.0",
|
|
461
766
|
id: message.id,
|
|
@@ -468,10 +773,12 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
468
773
|
},
|
|
469
774
|
});
|
|
470
775
|
default:
|
|
776
|
+
telemetryRequestErrorCategory = "not_found";
|
|
471
777
|
return jsonRpcError(message.id ?? null, -32601, `Method not found: ${message.method}`, 404);
|
|
472
778
|
}
|
|
473
779
|
}
|
|
474
780
|
catch (error) {
|
|
781
|
+
telemetryRequestErrorCategory = classifyTelemetryError(error);
|
|
475
782
|
if (message.method === "tools/call")
|
|
476
783
|
return json({
|
|
477
784
|
jsonrpc: "2.0",
|
|
@@ -489,13 +796,35 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
489
796
|
const code = message.method === "skills/get" ||
|
|
490
797
|
message.method === "resources/read" ||
|
|
491
798
|
message.method === "resources/directory/read" ||
|
|
492
|
-
message.method === "subscriptions/listen"
|
|
799
|
+
message.method === "subscriptions/listen" ||
|
|
800
|
+
message.method === "skills/list"
|
|
493
801
|
? -32602
|
|
494
802
|
: -32603;
|
|
495
803
|
return jsonRpcError(message.id ?? null, code, error instanceof Error ? error.message : String(error), code === -32602 ? 400 : 500);
|
|
496
804
|
}
|
|
805
|
+
finally {
|
|
806
|
+
const requestDurationMs = performance.now() - requestStartedAt;
|
|
807
|
+
sessionActiveRequestDurationMs += requestDurationMs;
|
|
808
|
+
sessionLastActivityAt = performance.now();
|
|
809
|
+
recordMcpRequest();
|
|
810
|
+
}
|
|
497
811
|
};
|
|
812
|
+
handler.telemetrySnapshot = () => telemetryRecorder.snapshot();
|
|
498
813
|
handler.closeSubscriptions = () => {
|
|
814
|
+
if (!sessionRecorded && sessionRequestCount > 0 && telemetry.level !== "none") {
|
|
815
|
+
sessionRecorded = true;
|
|
816
|
+
telemetryRecorder.record({
|
|
817
|
+
event: "session",
|
|
818
|
+
wallClockDurationMs: Math.max(0, (sessionLastActivityAt ?? performance.now()) - (sessionStartedAt ?? performance.now())),
|
|
819
|
+
activeRequestDurationMs: sessionActiveRequestDurationMs,
|
|
820
|
+
idleGapDurationMs: Math.max(0, (sessionLastActivityAt ?? performance.now()) -
|
|
821
|
+
(sessionStartedAt ?? performance.now()) -
|
|
822
|
+
sessionActiveRequestDurationMs),
|
|
823
|
+
requestCount: sessionRequestCount,
|
|
824
|
+
toolCallCount: sessionToolCallCount,
|
|
825
|
+
commandCallCount: sessionCommandCallCount,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
499
828
|
for (const subscription of subscriptions) {
|
|
500
829
|
writeSse(subscription.controller, {
|
|
501
830
|
jsonrpc: "2.0",
|
|
@@ -511,31 +840,61 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
511
840
|
subscription.controller.close();
|
|
512
841
|
}
|
|
513
842
|
subscriptions.clear();
|
|
843
|
+
void codeGraph.close();
|
|
514
844
|
};
|
|
515
845
|
return handler;
|
|
516
846
|
}
|
|
517
|
-
function
|
|
847
|
+
async function availableTools(codeGraph, gitEnabled) {
|
|
848
|
+
const capability = await codeGraph.capability();
|
|
849
|
+
const tools = capability.status === "available" ||
|
|
850
|
+
capability.status === "startup_failed" ||
|
|
851
|
+
capability.status === "runtime_failed"
|
|
852
|
+
? [...baseTools, codeGraphTool]
|
|
853
|
+
: baseTools;
|
|
854
|
+
return gitEnabled ? tools : tools.filter((item) => !item.name.startsWith("git_"));
|
|
855
|
+
}
|
|
856
|
+
function buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, codeGraph) {
|
|
518
857
|
const npm = npmAllowed.length ? npmAllowed.map((item) => `- ${item}`).join("\n") : "- none";
|
|
519
858
|
const commands = allowedCommands.length
|
|
520
859
|
? allowedCommands.map((item) => `- ${item}`).join("\n")
|
|
521
860
|
: "- none";
|
|
522
|
-
|
|
861
|
+
const blacklist = blacklistedCommands.length
|
|
862
|
+
? blacklistedCommands.map((item) => `- ${item}`).join("\n")
|
|
863
|
+
: "- none";
|
|
864
|
+
const codeGraphSection = codeGraph.status === "available" ||
|
|
865
|
+
codeGraph.status === "startup_failed" ||
|
|
866
|
+
codeGraph.status === "runtime_failed"
|
|
867
|
+
? "\n\n## CodeGraph\n- CodeGraph is an optional code-intelligence backend for this project.\n- Prefer `codegraph_explore` for structural, symbol, and code-flow questions when available.\n- Agent Dir pins CodeGraph to this project root; do not attempt to select another project.\n- Current status: " +
|
|
868
|
+
codeGraph.status +
|
|
869
|
+
(codeGraph.detail ? ` — ${codeGraph.detail}` : "") +
|
|
870
|
+
"."
|
|
871
|
+
: "";
|
|
872
|
+
return `# Agent Dir operating instructions\n\nUse Agent Dir as the primary project interface. Optimize for correctness with the minimum necessary tool calls, filesystem reads, command output, and context.\n\n## Efficiency rules\n- Start with the narrowest operation that can answer the question. Do not dump the repository, large files, or full command output when a targeted operation is sufficient.\n- Prefer project_context for initial orientation; it combines project structure, package metadata, and execution policy in one call. Use project_overview when package metadata is unnecessary.\n- Prefer find_files for known filename patterns and list_dirs for one directory. Use list_files only when recursive discovery is genuinely required.\n- Prefer search_code/search_files before reading files. Search first, then read only the relevant ranges. Use read_relevant when one call can locate and return the needed source context.\n- Prefer find_symbol/find_definition/find_references for known symbols instead of scanning source files manually.\n- Prefer read_range for a bounded section. Use read_files to batch several already-identified files.\n- Prefer patch_files for targeted edits. Do not rewrite an entire file when a small exact replacement is sufficient.\n- Batch related reads, writes, patches, searches, and commands into one tool call when practical.\n- Keep maxResults and Git log limits small unless the initial result is insufficient.\n- Prefer dedicated Agent Dir tools over run_command_batch because dedicated tools return structured, bounded results.\n- Prefer diagnostics before expensive tests, linters, or typechecks when checking basic structural issues.\n- Run the narrowest relevant validation after a change; escalate only when required by the task or release workflow.\n- Before a broad Git diff, use git_status; then inspect only the relevant path or staged diff.\n- Never repeat a successful discovery/read just because another tool can provide the same information.\n\n## Preferred workflow\n1. Orient: project_overview.\n2. Locate: find_files/search_code/find_symbol/find_definition as appropriate.\n3. Read: read_range or batched read_files.\n4. Modify: patch_files for targeted changes; write_files for new/complete files.\n5. Validate: diagnostics first when applicable, then the narrowest relevant npm script.\n6. Review: git_status, then scoped git_diff when needed.\n\n## Execution policy\nAllowed npm scripts:\n${npm}\n\nAllowed executables:\n${commands}\n\nBlacklisted command prefixes:\n${blacklist}\n\nOnly use commands from the execution policy. A blacklisted command prefix overrides an allowed executable. Do not attempt to bypass it with shells or alternate executables.\n\n## Git capability\nDedicated Git MCP tools are ${gitEnabled ? "enabled" : "disabled"} for this profile.\n\n## Tool selection\n- Discovery: project_context > locate > project_overview > find_files/list_dirs > list_files.\n- Code location: inspect_symbol > find_definition/find_symbol > locate > search_code > broad file reads.\n- File reading: read_relevant > read_range > targeted read_files > broad recursive reads.\n- Editing: patch_files > write_files for complete files.\n- Validation: diagnostics > targeted npm script > full check/test.\n- Git inspection: git_changes > git_status > scoped git_diff > full repository diff.\n\nThe execution policy and this guidance are generated from the running Agent Dir configuration; do not maintain a separate client-specific copy.${codeGraphSection}`;
|
|
523
873
|
}
|
|
524
|
-
function buildAgentCapabilities(npmAllowed, allowedCommands) {
|
|
874
|
+
function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled) {
|
|
525
875
|
return {
|
|
526
876
|
version: packageVersion,
|
|
527
|
-
tools: baseTools
|
|
877
|
+
tools: baseTools
|
|
878
|
+
.filter((item) => gitEnabled || !item.name.startsWith("git_"))
|
|
879
|
+
.map((item) => item.name),
|
|
528
880
|
resources: [INSTRUCTIONS_URI, CAPABILITIES_URI],
|
|
529
|
-
execution: {
|
|
881
|
+
execution: {
|
|
882
|
+
npmScripts: [...npmAllowed],
|
|
883
|
+
commands: [...allowedCommands],
|
|
884
|
+
blacklistedCommands: [...blacklistedCommands],
|
|
885
|
+
git: gitEnabled,
|
|
886
|
+
},
|
|
530
887
|
efficiency: {
|
|
531
888
|
preferred: {
|
|
532
|
-
orientation: "
|
|
889
|
+
orientation: "project_context",
|
|
890
|
+
focusedReading: "read_relevant",
|
|
891
|
+
changeReview: "git_changes",
|
|
533
892
|
discovery: ["find_files", "list_dirs", "search_code"],
|
|
534
893
|
symbolNavigation: ["find_definition", "find_symbol", "find_references"],
|
|
535
894
|
reading: ["read_range", "read_files"],
|
|
536
|
-
editing: ["patch_files", "write_files"],
|
|
537
|
-
validation: ["diagnostics", "targeted npm script"],
|
|
538
|
-
gitInspection: ["git_status", "git_diff", "git_log"],
|
|
895
|
+
editing: ["apply_changes", "patch_files", "write_files"],
|
|
896
|
+
validation: ["validate", "diagnostics", "targeted npm script"],
|
|
897
|
+
gitInspection: ["git_changes", "git_status", "git_diff", "git_log"],
|
|
539
898
|
},
|
|
540
899
|
avoid: [
|
|
541
900
|
"recursive discovery when targeted search is enough",
|
|
@@ -554,37 +913,105 @@ function capabilities() {
|
|
|
554
913
|
extensions: { "io.modelcontextprotocol/skills": { directoryRead: true } },
|
|
555
914
|
};
|
|
556
915
|
}
|
|
557
|
-
async function callTool(name, args, root, npmAllowed, allowedCommands, notify, signal) {
|
|
916
|
+
async function callTool(name, args, root, npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, notify, signal, codeGraph) {
|
|
558
917
|
let structuredContent;
|
|
918
|
+
if (name === "codegraph_explore") {
|
|
919
|
+
if (!codeGraph)
|
|
920
|
+
throw new Error("CodeGraph integration is unavailable.");
|
|
921
|
+
if (typeof args.query !== "string" || args.query.trim().length === 0)
|
|
922
|
+
throw new Error("query must be a non-empty string.");
|
|
923
|
+
if (args.maxFiles !== undefined &&
|
|
924
|
+
(!Number.isInteger(args.maxFiles) || Number(args.maxFiles) < 1 || Number(args.maxFiles) > 100))
|
|
925
|
+
throw new Error("maxFiles must be an integer between 1 and 100.");
|
|
926
|
+
return await codeGraph.explore(args.query, args.maxFiles === undefined ? undefined : Number(args.maxFiles));
|
|
927
|
+
}
|
|
928
|
+
if (name.startsWith("git_") && !gitEnabled) {
|
|
929
|
+
throw new Error("Dedicated Git tools are disabled for this profile.");
|
|
930
|
+
}
|
|
559
931
|
switch (name) {
|
|
560
|
-
case "list_files":
|
|
561
|
-
|
|
932
|
+
case "list_files": {
|
|
933
|
+
const items = await listFiles(root);
|
|
934
|
+
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
562
935
|
break;
|
|
563
|
-
|
|
564
|
-
|
|
936
|
+
}
|
|
937
|
+
case "list_dirs": {
|
|
938
|
+
const items = await listDirs(root, stringArray(args.paths, "paths"));
|
|
939
|
+
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
565
940
|
break;
|
|
566
|
-
|
|
567
|
-
|
|
941
|
+
}
|
|
942
|
+
case "read_range": {
|
|
943
|
+
const result = await readRange(root, String(args.path), Number(args.startLine), Number(args.endLine));
|
|
944
|
+
const bounded = truncateText(result.content, outputBytesArg(args.maxBytes));
|
|
945
|
+
structuredContent = { ...result, content: bounded.text };
|
|
568
946
|
break;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
947
|
+
}
|
|
948
|
+
case "read_files": {
|
|
949
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
950
|
+
let remaining = maxBytes;
|
|
951
|
+
const output = [];
|
|
952
|
+
for (const p of stringArray(args.paths, "paths")) {
|
|
953
|
+
const content = await readFile(root, p);
|
|
954
|
+
const bounded = truncateText(content, remaining);
|
|
955
|
+
output.push({ path: p, content: bounded.text });
|
|
956
|
+
remaining -= Buffer.byteLength(bounded.text, "utf8");
|
|
957
|
+
if (bounded.truncated || remaining <= 0)
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
structuredContent = output;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
case "apply_changes": {
|
|
964
|
+
const changes = objectArray(args.changes, "changes").map((change) => {
|
|
965
|
+
const kind = String(change.kind);
|
|
966
|
+
if (kind !== "write" && kind !== "patch" && kind !== "delete")
|
|
967
|
+
throw new Error("Change kind must be write, patch, or delete.");
|
|
968
|
+
if (kind === "write" && typeof change.content !== "string")
|
|
969
|
+
throw new Error("write changes require content.");
|
|
970
|
+
if (kind === "patch" && !Array.isArray(change.patches))
|
|
971
|
+
throw new Error("patch changes require patches.");
|
|
972
|
+
return {
|
|
973
|
+
kind,
|
|
974
|
+
path: String(change.path),
|
|
975
|
+
...(kind === "write" ? { content: String(change.content) } : {}),
|
|
976
|
+
...(kind === "patch"
|
|
977
|
+
? {
|
|
978
|
+
patches: objectArray(change.patches, "patches").map((patch) => ({
|
|
979
|
+
search: String(patch.search),
|
|
980
|
+
replace: String(patch.replace),
|
|
981
|
+
...(patch.count === undefined ? {} : { count: Number(patch.count) }),
|
|
982
|
+
})),
|
|
983
|
+
}
|
|
984
|
+
: {}),
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
const dryRun = args.dryRun === true;
|
|
988
|
+
const affected = [...new Set(changes.map((change) => change.path))];
|
|
989
|
+
const before = new Map();
|
|
990
|
+
for (const p of affected)
|
|
991
|
+
before.set(p, await skillResourceUrisForPath(root, p));
|
|
992
|
+
structuredContent = await applyFileChanges(root, changes, dryRun);
|
|
993
|
+
if (!dryRun)
|
|
994
|
+
for (const p of affected)
|
|
995
|
+
await notifyResourceChange(notify, root, p, before.get(p) ?? []);
|
|
574
996
|
break;
|
|
997
|
+
}
|
|
575
998
|
case "write_files": {
|
|
999
|
+
const dryRun = args.dryRun === true;
|
|
576
1000
|
const output = [];
|
|
577
1001
|
for (const file of objectArray(args.files, "files")) {
|
|
578
1002
|
const p = String(file.path);
|
|
579
1003
|
const before = await skillResourceUrisForPath(root, p);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1004
|
+
if (!dryRun) {
|
|
1005
|
+
await writeFile(root, p, String(file.content));
|
|
1006
|
+
await notifyResourceChange(notify, root, p, before);
|
|
1007
|
+
}
|
|
1008
|
+
output.push({ path: p, written: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
583
1009
|
}
|
|
584
1010
|
structuredContent = output;
|
|
585
1011
|
break;
|
|
586
1012
|
}
|
|
587
1013
|
case "patch_files": {
|
|
1014
|
+
const dryRun = args.dryRun === true;
|
|
588
1015
|
const files = objectArray(args.files, "files").map((file) => ({
|
|
589
1016
|
path: String(file.path),
|
|
590
1017
|
patches: objectArray(file.patches, "patches").map((p) => ({
|
|
@@ -596,70 +1023,331 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
596
1023
|
const before = new Map();
|
|
597
1024
|
for (const file of files)
|
|
598
1025
|
before.set(file.path, await skillResourceUrisForPath(root, file.path));
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1026
|
+
if (dryRun) {
|
|
1027
|
+
structuredContent = await patchFilesDryRun(root, files);
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
structuredContent = await patchFiles(root, files);
|
|
1031
|
+
}
|
|
1032
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1033
|
+
if (Array.isArray(structuredContent)) {
|
|
1034
|
+
structuredContent = boundedItems(structuredContent.map((item) => {
|
|
1035
|
+
if (!isRecord(item) || typeof item.content !== "string")
|
|
1036
|
+
return item;
|
|
1037
|
+
return { ...item, content: truncateText(item.content, maxBytes).text };
|
|
1038
|
+
}), maxBytes).items;
|
|
1039
|
+
}
|
|
1040
|
+
if (!dryRun) {
|
|
1041
|
+
for (const file of files)
|
|
1042
|
+
await notifyResourceChange(notify, root, file.path, before.get(file.path) ?? []);
|
|
1043
|
+
}
|
|
602
1044
|
break;
|
|
603
1045
|
}
|
|
604
1046
|
case "delete_files": {
|
|
1047
|
+
const dryRun = args.dryRun === true;
|
|
605
1048
|
const output = [];
|
|
606
1049
|
for (const p of stringArray(args.paths, "paths")) {
|
|
607
1050
|
const before = await skillResourceUrisForPath(root, p);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1051
|
+
if (!dryRun) {
|
|
1052
|
+
await deleteFile(root, p);
|
|
1053
|
+
await notifyResourceChange(notify, root, p, before);
|
|
1054
|
+
}
|
|
1055
|
+
output.push({ path: p, deleted: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
611
1056
|
}
|
|
612
1057
|
structuredContent = output;
|
|
613
1058
|
break;
|
|
614
1059
|
}
|
|
615
1060
|
case "search_files":
|
|
616
|
-
structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000));
|
|
1061
|
+
structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
617
1062
|
break;
|
|
618
1063
|
case "find_files":
|
|
619
|
-
structuredContent = await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000));
|
|
1064
|
+
structuredContent = boundedItems(await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
620
1065
|
break;
|
|
621
1066
|
case "search_code":
|
|
622
|
-
structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000));
|
|
1067
|
+
structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
623
1068
|
break;
|
|
624
1069
|
case "find_symbol":
|
|
625
|
-
structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500));
|
|
1070
|
+
structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
626
1071
|
break;
|
|
627
1072
|
case "find_definition":
|
|
628
|
-
structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500));
|
|
1073
|
+
structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
629
1074
|
break;
|
|
630
1075
|
case "find_references":
|
|
631
|
-
structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000));
|
|
1076
|
+
structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
632
1077
|
break;
|
|
633
1078
|
case "find_imports":
|
|
634
|
-
structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000));
|
|
1079
|
+
structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
635
1080
|
break;
|
|
636
1081
|
case "find_exports":
|
|
637
|
-
structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000));
|
|
1082
|
+
structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
638
1083
|
break;
|
|
639
1084
|
case "git_status":
|
|
640
|
-
structuredContent = await gitStatus(root);
|
|
1085
|
+
structuredContent = boundCommandResult(await gitStatus(root), outputBytesArg(args.maxBytes));
|
|
641
1086
|
break;
|
|
642
1087
|
case "git_stage":
|
|
643
|
-
structuredContent = await gitStage(root, stringArray(args.paths, "paths"));
|
|
1088
|
+
structuredContent = boundCommandResult(await gitStage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
644
1089
|
break;
|
|
645
1090
|
case "git_unstage":
|
|
646
|
-
structuredContent = await gitUnstage(root, stringArray(args.paths, "paths"));
|
|
1091
|
+
structuredContent = boundCommandResult(await gitUnstage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
647
1092
|
break;
|
|
648
1093
|
case "git_commit":
|
|
649
|
-
structuredContent = await gitCommit(root, String(args.message));
|
|
1094
|
+
structuredContent = boundCommandResult(await gitCommit(root, String(args.message)), outputBytesArg(args.maxBytes));
|
|
650
1095
|
break;
|
|
651
1096
|
case "git_restore":
|
|
652
|
-
structuredContent = await gitRestore(root, stringArray(args.paths, "paths"));
|
|
1097
|
+
structuredContent = boundCommandResult(await gitRestore(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
653
1098
|
break;
|
|
654
1099
|
case "git_push":
|
|
655
|
-
structuredContent = await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch));
|
|
1100
|
+
structuredContent = boundCommandResult(await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch)), outputBytesArg(args.maxBytes));
|
|
656
1101
|
break;
|
|
657
1102
|
case "git_diff":
|
|
658
|
-
structuredContent = await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path));
|
|
1103
|
+
structuredContent = boundCommandResult(await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
659
1104
|
break;
|
|
660
1105
|
case "git_log":
|
|
661
|
-
structuredContent = await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path));
|
|
1106
|
+
structuredContent = boundCommandResult(await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
1107
|
+
break;
|
|
1108
|
+
case "locate": {
|
|
1109
|
+
const query = String(args.query);
|
|
1110
|
+
const kind = typeof args.kind === "string" ? args.kind : "auto";
|
|
1111
|
+
const regex = args.regex === true;
|
|
1112
|
+
const maxResults = numberArg(args.maxResults, 20, 100);
|
|
1113
|
+
const resolvedKind = kind === "auto"
|
|
1114
|
+
? /[*?]/.test(query)
|
|
1115
|
+
? "file"
|
|
1116
|
+
: /\.(ts|tsx|js|jsx|py|go|rs|java|json|md|yml|yaml|css|html)$/.test(query)
|
|
1117
|
+
? "file"
|
|
1118
|
+
: "code"
|
|
1119
|
+
: kind;
|
|
1120
|
+
const items = resolvedKind === "file"
|
|
1121
|
+
? await findFiles(root, query, maxResults)
|
|
1122
|
+
: resolvedKind === "text"
|
|
1123
|
+
? await searchFiles(root, query, regex, maxResults)
|
|
1124
|
+
: await searchCode(root, query, regex, maxResults);
|
|
1125
|
+
const bounded = boundedItems(items, outputBytesArg(args.maxBytes));
|
|
1126
|
+
structuredContent = {
|
|
1127
|
+
query,
|
|
1128
|
+
kind: resolvedKind,
|
|
1129
|
+
items: bounded.items,
|
|
1130
|
+
count: bounded.items.length,
|
|
1131
|
+
truncated: bounded.truncated || items.length >= maxResults,
|
|
1132
|
+
};
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
case "inspect_symbol": {
|
|
1136
|
+
const symbol = String(args.symbol);
|
|
1137
|
+
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1138
|
+
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1139
|
+
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1140
|
+
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1141
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1142
|
+
const definitions = await findDefinition(root, symbol, maxResults, maxBytes);
|
|
1143
|
+
const definitionContext = await Promise.all(definitions.map(async (definition) => {
|
|
1144
|
+
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
1145
|
+
return { ...definition, context: range.content };
|
|
1146
|
+
}));
|
|
1147
|
+
const references = args.includeReferences === true
|
|
1148
|
+
? await findReferences(root, symbol, maxResults, maxBytes)
|
|
1149
|
+
: undefined;
|
|
1150
|
+
const result = {
|
|
1151
|
+
symbol,
|
|
1152
|
+
definitions: definitionContext,
|
|
1153
|
+
...(references ? { references } : {}),
|
|
1154
|
+
};
|
|
1155
|
+
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1156
|
+
structuredContent = bounded.truncated
|
|
1157
|
+
? { symbol, result: bounded.text, truncated: true }
|
|
1158
|
+
: result;
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
case "inspect": {
|
|
1162
|
+
const target = String(args.target);
|
|
1163
|
+
const requestedKind = typeof args.kind === "string" ? args.kind : "auto";
|
|
1164
|
+
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1165
|
+
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1166
|
+
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1167
|
+
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1168
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1169
|
+
let kind = requestedKind;
|
|
1170
|
+
if (kind === "auto") {
|
|
1171
|
+
try {
|
|
1172
|
+
await readFile(root, target);
|
|
1173
|
+
kind = "file";
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
kind = /[*?]/.test(target) ? "code" : "symbol";
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (kind === "file") {
|
|
1180
|
+
const content = await readFile(root, target);
|
|
1181
|
+
const boundedContent = truncateText(content, Math.max(1, maxBytes - 128));
|
|
1182
|
+
const result = {
|
|
1183
|
+
target,
|
|
1184
|
+
kind,
|
|
1185
|
+
path: target,
|
|
1186
|
+
content: boundedContent.text,
|
|
1187
|
+
truncated: boundedContent.truncated,
|
|
1188
|
+
};
|
|
1189
|
+
structuredContent = result;
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
if (kind === "symbol") {
|
|
1193
|
+
const definitions = await findDefinition(root, target, maxResults, maxBytes);
|
|
1194
|
+
const definitionsWithContext = await Promise.all(definitions.map(async (definition) => {
|
|
1195
|
+
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
1196
|
+
return { ...definition, context: range.content };
|
|
1197
|
+
}));
|
|
1198
|
+
const references = args.includeReferences === true
|
|
1199
|
+
? await findReferences(root, target, maxResults, maxBytes)
|
|
1200
|
+
: undefined;
|
|
1201
|
+
const result = {
|
|
1202
|
+
target,
|
|
1203
|
+
kind,
|
|
1204
|
+
definitions: definitionsWithContext,
|
|
1205
|
+
...(references ? { references } : {}),
|
|
1206
|
+
};
|
|
1207
|
+
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1208
|
+
structuredContent = bounded.truncated
|
|
1209
|
+
? { target, kind, result: bounded.text, truncated: true }
|
|
1210
|
+
: result;
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
const matches = await searchCode(root, target, false, maxResults, maxBytes);
|
|
1214
|
+
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1215
|
+
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1216
|
+
return {
|
|
1217
|
+
...match,
|
|
1218
|
+
context: range.content,
|
|
1219
|
+
startLine: range.startLine,
|
|
1220
|
+
endLine: range.endLine,
|
|
1221
|
+
};
|
|
1222
|
+
}));
|
|
1223
|
+
const bounded = boundedItems(relevant, maxBytes);
|
|
1224
|
+
structuredContent = {
|
|
1225
|
+
target,
|
|
1226
|
+
kind: "code",
|
|
1227
|
+
matches: bounded.items,
|
|
1228
|
+
count: bounded.items.length,
|
|
1229
|
+
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1230
|
+
};
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
case "project_context": {
|
|
1234
|
+
const overview = await projectOverview(root);
|
|
1235
|
+
let pkg = { path: "package.json", exists: false };
|
|
1236
|
+
try {
|
|
1237
|
+
const packageJson = JSON.parse(await readFile(root, "package.json"));
|
|
1238
|
+
pkg = {
|
|
1239
|
+
path: "package.json",
|
|
1240
|
+
name: packageJson.name ?? null,
|
|
1241
|
+
version: packageJson.version ?? null,
|
|
1242
|
+
description: packageJson.description ?? null,
|
|
1243
|
+
packageManager: packageJson.packageManager ?? null,
|
|
1244
|
+
scripts: packageJson.scripts ?? {},
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
// Package metadata is optional; project context remains useful without it.
|
|
1249
|
+
}
|
|
1250
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1251
|
+
const bounded = truncateText(JSON.stringify({
|
|
1252
|
+
...overview,
|
|
1253
|
+
package: pkg,
|
|
1254
|
+
execution: { npmScripts: [...npmAllowed], commands: [...allowedCommands] },
|
|
1255
|
+
}), maxBytes);
|
|
1256
|
+
structuredContent = bounded.truncated
|
|
1257
|
+
? { result: bounded.text, truncated: true }
|
|
1258
|
+
: JSON.parse(bounded.text);
|
|
662
1259
|
break;
|
|
1260
|
+
}
|
|
1261
|
+
case "read_relevant": {
|
|
1262
|
+
const query = String(args.query);
|
|
1263
|
+
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1264
|
+
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1265
|
+
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1266
|
+
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1267
|
+
const matches = await searchCode(root, query, Boolean(args.regex), maxResults, outputBytesArg(args.maxBytes));
|
|
1268
|
+
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1269
|
+
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1270
|
+
return {
|
|
1271
|
+
...match,
|
|
1272
|
+
context: range.content,
|
|
1273
|
+
startLine: range.startLine,
|
|
1274
|
+
endLine: range.endLine,
|
|
1275
|
+
};
|
|
1276
|
+
}));
|
|
1277
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1278
|
+
const bounded = boundedItems(relevant, Math.max(1, maxBytes - 128));
|
|
1279
|
+
const result = {
|
|
1280
|
+
query,
|
|
1281
|
+
matches: bounded.items,
|
|
1282
|
+
count: relevant.length,
|
|
1283
|
+
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1284
|
+
};
|
|
1285
|
+
structuredContent = result;
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
case "validate": {
|
|
1289
|
+
const level = typeof args.level === "string" ? args.level : "diagnostics";
|
|
1290
|
+
const maxResults = numberArg(args.maxResults, 200, 1000);
|
|
1291
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1292
|
+
const diagnosticItems = await diagnostics(root, maxResults);
|
|
1293
|
+
const scripts = level === "diagnostics"
|
|
1294
|
+
? []
|
|
1295
|
+
: args.scripts !== undefined
|
|
1296
|
+
? stringArray(args.scripts, "scripts")
|
|
1297
|
+
: level === "full"
|
|
1298
|
+
? ["check", "test"].filter((script) => npmAllowed.includes(script))
|
|
1299
|
+
: [];
|
|
1300
|
+
const checks = [
|
|
1301
|
+
{
|
|
1302
|
+
kind: "diagnostics",
|
|
1303
|
+
ok: !diagnosticItems.some((item) => item.severity === "error"),
|
|
1304
|
+
count: diagnosticItems.length,
|
|
1305
|
+
issues: boundedItems(diagnosticItems, maxBytes).items,
|
|
1306
|
+
},
|
|
1307
|
+
];
|
|
1308
|
+
for (const script of scripts) {
|
|
1309
|
+
const result = await runNpm(root, script, 120000, npmAllowed, signal);
|
|
1310
|
+
checks.push({
|
|
1311
|
+
kind: "npm",
|
|
1312
|
+
script,
|
|
1313
|
+
ok: result.exitCode === 0,
|
|
1314
|
+
result,
|
|
1315
|
+
});
|
|
1316
|
+
if (result.exitCode !== 0)
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
structuredContent = {
|
|
1320
|
+
level,
|
|
1321
|
+
ok: checks.every((check) => check.ok === true),
|
|
1322
|
+
checks,
|
|
1323
|
+
scriptsRequested: scripts,
|
|
1324
|
+
};
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
case "git_changes": {
|
|
1328
|
+
const status = await gitStatus(root);
|
|
1329
|
+
const includeDiff = Boolean(args.includeDiff);
|
|
1330
|
+
const path = args.path === undefined ? undefined : String(args.path);
|
|
1331
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1332
|
+
const diff = includeDiff ? await gitDiff(root, false, path) : undefined;
|
|
1333
|
+
const statusBytes = Buffer.byteLength(JSON.stringify(status), "utf8");
|
|
1334
|
+
const diffBudget = Math.max(0, maxBytes - statusBytes - 64);
|
|
1335
|
+
const boundedDiff = diff ? truncateText(diff.stdout, diffBudget) : undefined;
|
|
1336
|
+
const result = {
|
|
1337
|
+
status,
|
|
1338
|
+
...(diff
|
|
1339
|
+
? {
|
|
1340
|
+
diff: {
|
|
1341
|
+
...diff,
|
|
1342
|
+
stdout: boundedDiff?.text ?? "",
|
|
1343
|
+
truncated: boundedDiff?.truncated ?? false,
|
|
1344
|
+
},
|
|
1345
|
+
}
|
|
1346
|
+
: {}),
|
|
1347
|
+
};
|
|
1348
|
+
structuredContent = result;
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
663
1351
|
case "project_overview":
|
|
664
1352
|
structuredContent = await projectOverview(root);
|
|
665
1353
|
break;
|
|
@@ -670,13 +1358,14 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
670
1358
|
structuredContent = {
|
|
671
1359
|
npmScripts: [...npmAllowed],
|
|
672
1360
|
commands: [...allowedCommands],
|
|
1361
|
+
blacklistedCommands: [...blacklistedCommands],
|
|
673
1362
|
};
|
|
674
1363
|
break;
|
|
675
1364
|
case "file_info":
|
|
676
1365
|
structuredContent = await fileInfo(root, String(args.path));
|
|
677
1366
|
break;
|
|
678
1367
|
case "diagnostics":
|
|
679
|
-
structuredContent = await diagnostics(root, numberArg(args.maxResults, 200, 1000));
|
|
1368
|
+
structuredContent = boundedItems(await diagnostics(root, numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
680
1369
|
break;
|
|
681
1370
|
case "run_npm_batch": {
|
|
682
1371
|
const output = [];
|
|
@@ -686,18 +1375,52 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
686
1375
|
if (value.exitCode !== 0)
|
|
687
1376
|
break;
|
|
688
1377
|
}
|
|
689
|
-
|
|
1378
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1379
|
+
const boundedOutput = [];
|
|
1380
|
+
let remaining = maxBytes - 2;
|
|
1381
|
+
for (const item of output) {
|
|
1382
|
+
const separatorBytes = boundedOutput.length ? 1 : 0;
|
|
1383
|
+
if (remaining <= separatorBytes)
|
|
1384
|
+
break;
|
|
1385
|
+
const budget = remaining - separatorBytes;
|
|
1386
|
+
const bounded = boundCommandResult(item, budget);
|
|
1387
|
+
const boundedBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
1388
|
+
if (boundedBytes > budget)
|
|
1389
|
+
break;
|
|
1390
|
+
boundedOutput.push(bounded);
|
|
1391
|
+
remaining -= separatorBytes + boundedBytes;
|
|
1392
|
+
if (remaining <= 0)
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
structuredContent = boundedOutput;
|
|
690
1396
|
break;
|
|
691
1397
|
}
|
|
692
1398
|
case "run_command_batch": {
|
|
693
1399
|
const output = [];
|
|
694
1400
|
for (const item of objectArray(args.commands, "commands")) {
|
|
695
|
-
const value = await runCommand(root, String(item.command), Array.isArray(item.args) ? item.args.map(String) : [], allowedCommands, 120000, signal);
|
|
1401
|
+
const value = await runCommand(root, String(item.command), Array.isArray(item.args) ? item.args.map(String) : [], allowedCommands, blacklistedCommands, 120000, signal);
|
|
696
1402
|
output.push(value);
|
|
697
1403
|
if (value.exitCode !== 0)
|
|
698
1404
|
break;
|
|
699
1405
|
}
|
|
700
|
-
|
|
1406
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1407
|
+
const boundedOutput = [];
|
|
1408
|
+
let remaining = maxBytes - 2;
|
|
1409
|
+
for (const item of output) {
|
|
1410
|
+
const separatorBytes = boundedOutput.length ? 1 : 0;
|
|
1411
|
+
if (remaining <= separatorBytes)
|
|
1412
|
+
break;
|
|
1413
|
+
const budget = remaining - separatorBytes;
|
|
1414
|
+
const bounded = boundCommandResult(item, budget);
|
|
1415
|
+
const boundedBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
1416
|
+
if (boundedBytes > budget)
|
|
1417
|
+
break;
|
|
1418
|
+
boundedOutput.push(bounded);
|
|
1419
|
+
remaining -= separatorBytes + boundedBytes;
|
|
1420
|
+
if (remaining <= 0)
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
structuredContent = boundedOutput;
|
|
701
1424
|
break;
|
|
702
1425
|
}
|
|
703
1426
|
default:
|
|
@@ -814,6 +1537,114 @@ function completeToolResult(structuredContent) {
|
|
|
814
1537
|
_meta: SERVER_INFO_META,
|
|
815
1538
|
};
|
|
816
1539
|
}
|
|
1540
|
+
function telemetryRuntime(params) {
|
|
1541
|
+
const clientInfo = isRecord(params.clientInfo) ? params.clientInfo : undefined;
|
|
1542
|
+
return {
|
|
1543
|
+
agentDirVersion: packageVersion,
|
|
1544
|
+
nodeMajor: Number(process.versions.node.split(".")[0]),
|
|
1545
|
+
os: process.platform === "darwin"
|
|
1546
|
+
? "macos"
|
|
1547
|
+
: process.platform === "linux"
|
|
1548
|
+
? "linux"
|
|
1549
|
+
: process.platform === "win32"
|
|
1550
|
+
? "windows"
|
|
1551
|
+
: "other",
|
|
1552
|
+
arch: process.arch,
|
|
1553
|
+
...(typeof clientInfo?.name === "string" ? { mcpClientName: clientInfo.name } : {}),
|
|
1554
|
+
...(typeof clientInfo?.version === "string" ? { mcpClientVersion: clientInfo.version } : {}),
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
function toolTelemetryMetrics(toolName, result) {
|
|
1558
|
+
const structured = result.structuredContent;
|
|
1559
|
+
if (!Array.isArray(structured) && !isRecord(structured))
|
|
1560
|
+
return {};
|
|
1561
|
+
const paths = new Set();
|
|
1562
|
+
let truncated = false;
|
|
1563
|
+
let paginated = false;
|
|
1564
|
+
const visit = (value) => {
|
|
1565
|
+
if (!isRecord(value))
|
|
1566
|
+
return;
|
|
1567
|
+
if (typeof value.path === "string")
|
|
1568
|
+
paths.add(value.path);
|
|
1569
|
+
if (value.truncated === true)
|
|
1570
|
+
truncated = true;
|
|
1571
|
+
if (value.nextCursor !== undefined)
|
|
1572
|
+
paginated = true;
|
|
1573
|
+
for (const child of Object.values(value)) {
|
|
1574
|
+
if (Array.isArray(child))
|
|
1575
|
+
for (const item of child)
|
|
1576
|
+
visit(item);
|
|
1577
|
+
else if (isRecord(child))
|
|
1578
|
+
visit(child);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
visit(structured);
|
|
1582
|
+
return {
|
|
1583
|
+
...(paths.size > 0 && /^(write|patch|delete|read|file|git_changes|inspect)/.test(toolName)
|
|
1584
|
+
? { affectedFiles: paths.size }
|
|
1585
|
+
: {}),
|
|
1586
|
+
...(truncated ? { truncated: true } : {}),
|
|
1587
|
+
...(paginated ? { paginated: true } : {}),
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function commandTelemetryDescriptor(toolName) {
|
|
1591
|
+
if (toolName === "run_npm_batch")
|
|
1592
|
+
return { family: "npm", operation: "batch" };
|
|
1593
|
+
if (toolName === "run_command_batch")
|
|
1594
|
+
return { family: "command", operation: "batch" };
|
|
1595
|
+
if (toolName === "validate")
|
|
1596
|
+
return { family: "npm", operation: "validate" };
|
|
1597
|
+
if (toolName.startsWith("git_"))
|
|
1598
|
+
return { family: "git", operation: toolName.slice("git_".length) };
|
|
1599
|
+
return undefined;
|
|
1600
|
+
}
|
|
1601
|
+
async function buildTelemetryProjectContext(root, codeGraph) {
|
|
1602
|
+
const overview = await projectOverview(root);
|
|
1603
|
+
const extension = overview.languages[0]?.extension;
|
|
1604
|
+
const language = extension === ".ts" || extension === ".tsx"
|
|
1605
|
+
? "typescript"
|
|
1606
|
+
: extension === ".js" || extension === ".jsx"
|
|
1607
|
+
? "javascript"
|
|
1608
|
+
: extension === ".py"
|
|
1609
|
+
? "python"
|
|
1610
|
+
: extension === ".go"
|
|
1611
|
+
? "go"
|
|
1612
|
+
: extension === ".rs"
|
|
1613
|
+
? "rust"
|
|
1614
|
+
: extension === ".java"
|
|
1615
|
+
? "java"
|
|
1616
|
+
: undefined;
|
|
1617
|
+
const packageManager = overview.packageManagers[0];
|
|
1618
|
+
const projectSize = overview.files < 100 ? "small" : overview.files < 1000 ? "medium" : "large";
|
|
1619
|
+
const capability = await codeGraph.capability();
|
|
1620
|
+
return {
|
|
1621
|
+
...(language ? { language } : {}),
|
|
1622
|
+
...(packageManager && ["npm", "pnpm", "yarn", "bun"].includes(packageManager)
|
|
1623
|
+
? { packageManager: packageManager }
|
|
1624
|
+
: {}),
|
|
1625
|
+
hasGit: overview.git.available,
|
|
1626
|
+
hasCodegraph: capability.installed && capability.indexed,
|
|
1627
|
+
projectSize,
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
function resultCount(result) {
|
|
1631
|
+
const structured = result.structuredContent;
|
|
1632
|
+
return Array.isArray(structured) ? structured.length : undefined;
|
|
1633
|
+
}
|
|
1634
|
+
function classifyTelemetryError(error) {
|
|
1635
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
1636
|
+
if (message.includes("not found"))
|
|
1637
|
+
return "not_found";
|
|
1638
|
+
if (message.includes("invalid") || message.includes("must be"))
|
|
1639
|
+
return "invalid_input";
|
|
1640
|
+
if (message.includes("blocked") || message.includes("not allowed"))
|
|
1641
|
+
return "authorization";
|
|
1642
|
+
if (message.includes("timeout") || message.includes("timed out"))
|
|
1643
|
+
return "timeout";
|
|
1644
|
+
if (message.includes("authentication") || message.includes("unauthorized"))
|
|
1645
|
+
return "authentication";
|
|
1646
|
+
return "execution";
|
|
1647
|
+
}
|
|
817
1648
|
function isRecord(value) {
|
|
818
1649
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
819
1650
|
}
|
|
@@ -830,6 +1661,69 @@ function objectArray(value, name) {
|
|
|
830
1661
|
throw new Error(`${name} must be an array of objects.`);
|
|
831
1662
|
return value;
|
|
832
1663
|
}
|
|
1664
|
+
function outputBytesArg(value) {
|
|
1665
|
+
const result = value === undefined ? DEFAULT_OUTPUT_BYTES : Number(value);
|
|
1666
|
+
if (!Number.isInteger(result) || result < 1024 || result > 1_000_000)
|
|
1667
|
+
throw new Error("maxBytes must be an integer between 1024 and 1000000.");
|
|
1668
|
+
return result;
|
|
1669
|
+
}
|
|
1670
|
+
function truncateText(value, maxBytes) {
|
|
1671
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
1672
|
+
return { text: value, truncated: false };
|
|
1673
|
+
if (maxBytes <= 32)
|
|
1674
|
+
return { text: "", truncated: true };
|
|
1675
|
+
const buffer = Buffer.from(value, "utf8");
|
|
1676
|
+
return {
|
|
1677
|
+
text: `${buffer.subarray(0, Math.max(0, maxBytes - 32)).toString("utf8")}\n…[truncated]`,
|
|
1678
|
+
truncated: true,
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
function boundCommandResult(value, maxBytes) {
|
|
1682
|
+
if (!isRecord(value))
|
|
1683
|
+
return value;
|
|
1684
|
+
const command = typeof value.command === "string" ? value.command : "";
|
|
1685
|
+
const stdout = typeof value.stdout === "string" ? value.stdout : "";
|
|
1686
|
+
const stderr = typeof value.stderr === "string" ? value.stderr : "";
|
|
1687
|
+
let commandBudget = Buffer.byteLength(command, "utf8");
|
|
1688
|
+
let stdoutBudget = Buffer.byteLength(stdout, "utf8");
|
|
1689
|
+
let stderrBudget = Buffer.byteLength(stderr, "utf8");
|
|
1690
|
+
const candidate = () => ({
|
|
1691
|
+
...value,
|
|
1692
|
+
command: truncateText(command, commandBudget).text,
|
|
1693
|
+
stdout: truncateText(stdout, stdoutBudget).text,
|
|
1694
|
+
stderr: truncateText(stderr, stderrBudget).text,
|
|
1695
|
+
});
|
|
1696
|
+
let result = candidate();
|
|
1697
|
+
while (Buffer.byteLength(JSON.stringify(result), "utf8") > maxBytes &&
|
|
1698
|
+
(commandBudget > 0 || stdoutBudget > 0 || stderrBudget > 0)) {
|
|
1699
|
+
const excess = Buffer.byteLength(JSON.stringify(result), "utf8") - maxBytes;
|
|
1700
|
+
if (stdoutBudget >= stderrBudget && stdoutBudget > 0)
|
|
1701
|
+
stdoutBudget = Math.max(0, stdoutBudget - excess);
|
|
1702
|
+
else if (stderrBudget > 0)
|
|
1703
|
+
stderrBudget = Math.max(0, stderrBudget - excess);
|
|
1704
|
+
else
|
|
1705
|
+
commandBudget = Math.max(0, commandBudget - excess);
|
|
1706
|
+
result = candidate();
|
|
1707
|
+
}
|
|
1708
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") <= maxBytes)
|
|
1709
|
+
return result;
|
|
1710
|
+
return { ...value, command: truncateText(command, commandBudget).text, stdout: "", stderr: "" };
|
|
1711
|
+
}
|
|
1712
|
+
function boundedItems(items, maxBytes) {
|
|
1713
|
+
const selected = [];
|
|
1714
|
+
let bytes = 2;
|
|
1715
|
+
for (const item of items) {
|
|
1716
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
1717
|
+
const separatorBytes = selected.length ? 1 : 0;
|
|
1718
|
+
if (selected.length && bytes + separatorBytes + itemBytes > maxBytes)
|
|
1719
|
+
break;
|
|
1720
|
+
if (!selected.length && bytes + itemBytes > maxBytes)
|
|
1721
|
+
break;
|
|
1722
|
+
selected.push(item);
|
|
1723
|
+
bytes += separatorBytes + itemBytes;
|
|
1724
|
+
}
|
|
1725
|
+
return { items: selected, truncated: selected.length < items.length };
|
|
1726
|
+
}
|
|
833
1727
|
function numberArg(value, fallback, max) {
|
|
834
1728
|
const result = value === undefined ? fallback : Number(value);
|
|
835
1729
|
if (!Number.isInteger(result) || result < 1 || result > max)
|
|
@@ -862,11 +1756,40 @@ function decodeCursor(value) {
|
|
|
862
1756
|
catch {
|
|
863
1757
|
throw new Error("Invalid pagination cursor.");
|
|
864
1758
|
}
|
|
1759
|
+
if (!/^(?:0|[1-9]\d*)$/.test(decoded))
|
|
1760
|
+
throw new Error("Invalid pagination cursor.");
|
|
865
1761
|
const offset = Number(decoded);
|
|
866
1762
|
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
867
1763
|
throw new Error("Invalid pagination cursor.");
|
|
868
1764
|
return offset;
|
|
869
1765
|
}
|
|
1766
|
+
async function patchFilesDryRun(root, files) {
|
|
1767
|
+
return Promise.all(files.map(async ({ path: relativePath, patches }) => {
|
|
1768
|
+
let content = await readFile(root, relativePath);
|
|
1769
|
+
for (const patch of patches) {
|
|
1770
|
+
if (!patch.search)
|
|
1771
|
+
throw new Error("Patch search text cannot be empty.");
|
|
1772
|
+
const occurrences = content.split(patch.search).length - 1;
|
|
1773
|
+
const count = patch.count ?? 1;
|
|
1774
|
+
if (occurrences === 0)
|
|
1775
|
+
throw new Error(`Patch text was not found in '${relativePath}'.`);
|
|
1776
|
+
if (!Number.isInteger(count) || count < 1)
|
|
1777
|
+
throw new Error("Patch count must be a positive integer.");
|
|
1778
|
+
if (occurrences < count)
|
|
1779
|
+
throw new Error(`Patch text occurs ${occurrences} time(s), but ${count} replacement(s) were requested.`);
|
|
1780
|
+
let offset = 0;
|
|
1781
|
+
for (let index = 0; index < count; index += 1) {
|
|
1782
|
+
const position = content.indexOf(patch.search, offset);
|
|
1783
|
+
content =
|
|
1784
|
+
content.slice(0, position) +
|
|
1785
|
+
patch.replace +
|
|
1786
|
+
content.slice(position + patch.search.length);
|
|
1787
|
+
offset = position + patch.replace.length;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return { path: relativePath, applied: patches.length, content, dryRun: true };
|
|
1791
|
+
}));
|
|
1792
|
+
}
|
|
870
1793
|
async function notifyResourceChange(notify, root, pathValue, before) {
|
|
871
1794
|
const after = await skillResourceUrisForPath(root, pathValue);
|
|
872
1795
|
if (before.length === 0 && after.length === 0)
|