agent-dir 0.0.0-stage → 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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -2
  3. package/dist/bin/agent-dir.d.ts +3 -0
  4. package/dist/bin/agent-dir.d.ts.map +1 -0
  5. package/dist/bin/agent-dir.js +201 -0
  6. package/dist/bin/agent-dir.js.map +1 -0
  7. package/dist/src/auth.d.ts +3 -0
  8. package/dist/src/auth.d.ts.map +1 -0
  9. package/dist/src/auth.js +18 -0
  10. package/dist/src/auth.js.map +1 -0
  11. package/dist/src/config.d.ts +6 -0
  12. package/dist/src/config.d.ts.map +1 -0
  13. package/dist/src/config.js +80 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/dist/src/logging.d.ts +16 -0
  16. package/dist/src/logging.d.ts.map +1 -0
  17. package/dist/src/logging.js +199 -0
  18. package/dist/src/logging.js.map +1 -0
  19. package/dist/src/mcp.d.ts +9 -0
  20. package/dist/src/mcp.d.ts.map +1 -0
  21. package/dist/src/mcp.js +824 -0
  22. package/dist/src/mcp.js.map +1 -0
  23. package/dist/src/server.d.ts +7 -0
  24. package/dist/src/server.d.ts.map +1 -0
  25. package/dist/src/server.js +343 -0
  26. package/dist/src/server.js.map +1 -0
  27. package/dist/src/tools/code.d.ts +27 -0
  28. package/dist/src/tools/code.d.ts.map +1 -0
  29. package/dist/src/tools/code.js +147 -0
  30. package/dist/src/tools/code.js.map +1 -0
  31. package/dist/src/tools/commands.d.ts +8 -0
  32. package/dist/src/tools/commands.d.ts.map +1 -0
  33. package/dist/src/tools/commands.js +61 -0
  34. package/dist/src/tools/commands.js.map +1 -0
  35. package/dist/src/tools/diagnostics.d.ts +9 -0
  36. package/dist/src/tools/diagnostics.d.ts.map +1 -0
  37. package/dist/src/tools/diagnostics.js +83 -0
  38. package/dist/src/tools/diagnostics.js.map +1 -0
  39. package/dist/src/tools/files.d.ts +39 -0
  40. package/dist/src/tools/files.d.ts.map +1 -0
  41. package/dist/src/tools/files.js +142 -0
  42. package/dist/src/tools/files.js.map +1 -0
  43. package/dist/src/tools/git.d.ts +15 -0
  44. package/dist/src/tools/git.d.ts.map +1 -0
  45. package/dist/src/tools/git.js +104 -0
  46. package/dist/src/tools/git.js.map +1 -0
  47. package/dist/src/tools/metadata.d.ts +20 -0
  48. package/dist/src/tools/metadata.d.ts.map +1 -0
  49. package/dist/src/tools/metadata.js +101 -0
  50. package/dist/src/tools/metadata.js.map +1 -0
  51. package/dist/src/tools/npm.d.ts +3 -0
  52. package/dist/src/tools/npm.d.ts.map +1 -0
  53. package/dist/src/tools/npm.js +76 -0
  54. package/dist/src/tools/npm.js.map +1 -0
  55. package/dist/src/tools/skills.d.ts +33 -0
  56. package/dist/src/tools/skills.d.ts.map +1 -0
  57. package/dist/src/tools/skills.js +240 -0
  58. package/dist/src/tools/skills.js.map +1 -0
  59. package/dist/src/tunnel.d.ts +3 -0
  60. package/dist/src/tunnel.d.ts.map +1 -0
  61. package/dist/src/tunnel.js +114 -0
  62. package/dist/src/tunnel.js.map +1 -0
  63. package/dist/src/types.d.ts +45 -0
  64. package/dist/src/types.d.ts.map +1 -0
  65. package/dist/src/types.js +2 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/package.json +51 -4
@@ -0,0 +1,824 @@
1
+ import { createRequire } from "node:module";
2
+ import { findDefinition, findExports, findFiles, findImports, findReferences, findSymbol, searchCode, searchFiles, } from "./tools/code.js";
3
+ import { runCommand } from "./tools/commands.js";
4
+ import { diagnostics } from "./tools/diagnostics.js";
5
+ import { deleteFile, listDirs, listFiles, patchFiles, readFile, readRange, writeFile, } from "./tools/files.js";
6
+ import { gitCommit, gitDiff, gitLog, gitPush, gitRestore, gitStage, gitStatus, gitUnstage, } from "./tools/git.js";
7
+ import { fileInfo, packageInfo, projectOverview } from "./tools/metadata.js";
8
+ import { runNpm } from "./tools/npm.js";
9
+ import { getSkill, listSkillResources, listSkills, readSkillDirectory, readSkillResource, skillResourceUrisForPath, } from "./tools/skills.js";
10
+ const require = createRequire(import.meta.url);
11
+ const packageVersion = require("../../package.json").version;
12
+ const PROTOCOL_VERSION = "2026-07-28";
13
+ const SERVER_INFO = { name: "agent-dir", version: packageVersion };
14
+ const SERVER_INFO_META = { "io.modelcontextprotocol/serverInfo": SERVER_INFO };
15
+ const PROTOCOL_META = "io.modelcontextprotocol/protocolVersion";
16
+ const CLIENT_CAPABILITIES_META = "io.modelcontextprotocol/clientCapabilities";
17
+ const PAGE_SIZE = 50;
18
+ const SUBSCRIPTION_ID_META = "io.modelcontextprotocol/subscriptionId";
19
+ const stringSchema = { type: "string" };
20
+ const pathSchema = { type: "string", description: "Path relative to the exposed project root." };
21
+ const objectOutput = (properties, required = []) => ({
22
+ type: "object",
23
+ properties,
24
+ ...(required.length ? { required } : {}),
25
+ });
26
+ const arrayOutput = (items) => ({
27
+ type: "array",
28
+ items,
29
+ });
30
+ const fileEntrySchema = objectOutput({ path: stringSchema, type: { type: "string", enum: ["file", "directory"] } }, ["path", "type"]);
31
+ const searchMatchSchema = objectOutput({
32
+ path: stringSchema,
33
+ line: { type: "integer" },
34
+ column: { type: "integer" },
35
+ text: stringSchema,
36
+ }, ["path", "line", "column", "text"]);
37
+ const commandResultSchema = objectOutput({
38
+ command: stringSchema,
39
+ exitCode: { type: "integer" },
40
+ stdout: stringSchema,
41
+ stderr: stringSchema,
42
+ }, ["command", "exitCode", "stdout", "stderr"]);
43
+ const baseTools = [
44
+ tool("list_files", "List all visible project files and directories recursively up to the server's discovery depth.", {}, arrayOutput(fileEntrySchema)),
45
+ tool("list_dirs", "List files and directories directly inside one or more project directories.", { paths: { type: "array", items: pathSchema, minItems: 1 } }, arrayOutput(objectOutput({ path: stringSchema, entries: arrayOutput(fileEntrySchema) }, [
46
+ "path",
47
+ "entries",
48
+ ])), ["paths"]),
49
+ tool("read_range", "Read a bounded line range from one UTF-8 project file.", {
50
+ path: pathSchema,
51
+ startLine: { type: "integer", minimum: 1 },
52
+ endLine: { type: "integer", minimum: 1 },
53
+ }, objectOutput({
54
+ path: stringSchema,
55
+ startLine: { type: "integer" },
56
+ endLine: { type: "integer" },
57
+ content: stringSchema,
58
+ }, ["path", "startLine", "endLine", "content"]), ["path", "startLine", "endLine"]),
59
+ tool("read_files", "Read one or more UTF-8 project files in one call.", { paths: { type: "array", items: pathSchema, minItems: 1 } }, arrayOutput(objectOutput({ path: stringSchema, content: stringSchema }, ["path", "content"])), ["paths"]),
60
+ tool("write_files", "Create or completely replace one or more UTF-8 project files.", {
61
+ files: {
62
+ type: "array",
63
+ items: objectOutput({ path: pathSchema, content: stringSchema }, ["path", "content"]),
64
+ minItems: 1,
65
+ },
66
+ }, arrayOutput(objectOutput({ path: stringSchema, written: { type: "boolean" } }, ["path", "written"])), ["files"]),
67
+ tool("patch_files", "Apply exact text replacements to one or more UTF-8 files.", {
68
+ files: {
69
+ type: "array",
70
+ items: objectOutput({
71
+ path: pathSchema,
72
+ patches: {
73
+ type: "array",
74
+ items: objectOutput({
75
+ search: stringSchema,
76
+ replace: stringSchema,
77
+ count: { type: "integer", minimum: 1 },
78
+ }, ["search", "replace"]),
79
+ minItems: 1,
80
+ },
81
+ }, ["path", "patches"]),
82
+ minItems: 1,
83
+ },
84
+ }, arrayOutput(objectOutput({ path: stringSchema, applied: { type: "integer" }, content: stringSchema }, [
85
+ "path",
86
+ "applied",
87
+ "content",
88
+ ])), ["files"]),
89
+ tool("delete_files", "Delete one or more project files.", { paths: { type: "array", items: pathSchema, minItems: 1 } }, arrayOutput(objectOutput({ path: stringSchema, deleted: { type: "boolean" } }, ["path", "deleted"])), ["paths"]),
90
+ tool("search_files", "Search text across visible project files.", {
91
+ query: stringSchema,
92
+ regex: { type: "boolean", default: false },
93
+ maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
94
+ }, arrayOutput(searchMatchSchema), ["query"]),
95
+ tool("find_files", "Find project files by a simple glob pattern using * and ?.", {
96
+ pattern: stringSchema,
97
+ maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
98
+ }, arrayOutput(stringSchema), ["pattern"]),
99
+ tool("search_code", "Search source-like project files for text or a regular expression.", {
100
+ query: stringSchema,
101
+ regex: { type: "boolean", default: false },
102
+ maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
103
+ }, arrayOutput(searchMatchSchema), ["query"]),
104
+ tool("find_symbol", "Find likely symbol definitions across source files.", {
105
+ symbol: stringSchema,
106
+ maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
107
+ }, arrayOutput(objectOutput({
108
+ ...searchMatchSchema.properties,
109
+ symbol: stringSchema,
110
+ kind: stringSchema,
111
+ }, ["path", "line", "column", "text", "symbol", "kind"])), ["symbol"]),
112
+ tool("find_definition", "Find likely definitions of a symbol.", {
113
+ symbol: stringSchema,
114
+ maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
115
+ }, arrayOutput(objectOutput({
116
+ ...searchMatchSchema.properties,
117
+ symbol: stringSchema,
118
+ kind: stringSchema,
119
+ }, ["path", "line", "column", "text", "symbol", "kind"])), ["symbol"]),
120
+ tool("find_references", "Find source-code references to a symbol.", {
121
+ symbol: stringSchema,
122
+ maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
123
+ }, arrayOutput(searchMatchSchema), ["symbol"]),
124
+ tool("find_imports", "Find import and require statements in source files.", { maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 } }, arrayOutput(objectOutput({
125
+ ...searchMatchSchema.properties,
126
+ module: stringSchema,
127
+ kind: { type: "string", enum: ["import", "require"] },
128
+ }, ["path", "line", "column", "text", "module", "kind"]))),
129
+ tool("find_exports", "Find exported declarations in source files.", { maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 } }, arrayOutput(objectOutput({
130
+ ...searchMatchSchema.properties,
131
+ symbol: stringSchema,
132
+ kind: { type: "string", const: "export" },
133
+ }, ["path", "line", "column", "text", "symbol", "kind"]))),
134
+ tool("git_status", "Show Git working-tree status and branch information.", {}, commandResultSchema),
135
+ tool("git_stage", "Stage one or more Git paths.", { paths: { type: "array", items: pathSchema, minItems: 1 } }, commandResultSchema, ["paths"]),
136
+ tool("git_unstage", "Unstage one or more Git paths without changing their working-tree contents.", { paths: { type: "array", items: pathSchema, minItems: 1 } }, commandResultSchema, ["paths"]),
137
+ tool("git_commit", "Create a Git commit with the currently staged changes.", { message: stringSchema }, commandResultSchema, ["message"]),
138
+ tool("git_restore", "Restore one or more Git paths from the index, discarding unstaged working-tree changes.", { paths: { type: "array", items: pathSchema, minItems: 1 } }, commandResultSchema, ["paths"]),
139
+ tool("git_push", "Push the current Git branch to a remote.", { remote: stringSchema, branch: stringSchema }, commandResultSchema),
140
+ tool("git_diff", "Show a read-only Git diff, optionally staged or limited to a path.", { staged: { type: "boolean", default: false }, path: pathSchema }, commandResultSchema),
141
+ tool("git_log", "Show recent Git commits in a compact, read-only form.", { limit: { type: "integer", minimum: 1, maximum: 200, default: 20 }, path: pathSchema }, commandResultSchema),
142
+ tool("project_overview", "Summarize project structure, languages, package managers, project metadata files, and Git state.", {}, objectOutput({
143
+ root: stringSchema,
144
+ files: { type: "integer" },
145
+ directories: { type: "integer" },
146
+ languages: { type: "array" },
147
+ packageManagers: { type: "array", items: stringSchema },
148
+ projectFiles: { type: "array", items: stringSchema },
149
+ git: { type: "object" },
150
+ }, ["root", "files", "directories", "languages", "packageManagers", "projectFiles", "git"])),
151
+ tool("package_info", "Read normalized metadata from package.json when present.", {}, { type: "object", additionalProperties: true }),
152
+ tool("file_info", "Return filesystem metadata for a project path.", { path: pathSchema }, { type: "object", additionalProperties: true }, ["path"]),
153
+ tool("diagnostics", "Run project-independent diagnostics without invoking tests, linters, or typecheckers.", { maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 } }, arrayOutput(objectOutput({
154
+ severity: { type: "string", enum: ["error", "warning", "info"] },
155
+ code: stringSchema,
156
+ path: stringSchema,
157
+ line: { type: "integer" },
158
+ message: stringSchema,
159
+ }, ["severity", "code", "message"]))),
160
+ tool("run_npm_batch", "Run one or more explicitly allowed npm scripts sequentially.", { scripts: { type: "array", items: stringSchema, minItems: 1 } }, arrayOutput(commandResultSchema), ["scripts"]),
161
+ tool("run_command_batch", "Run one or more explicitly allowed commands sequentially without a shell.", {
162
+ commands: {
163
+ type: "array",
164
+ items: objectOutput({ command: stringSchema, args: { type: "array", items: stringSchema } }, ["command"]),
165
+ minItems: 1,
166
+ },
167
+ }, arrayOutput(commandResultSchema), ["commands"]),
168
+ ];
169
+ function tool(name, description, properties, outputSchema, required = []) {
170
+ return {
171
+ name,
172
+ title: name.replace(/_/g, " "),
173
+ description,
174
+ inputSchema: {
175
+ type: "object",
176
+ properties,
177
+ ...(required.length ? { required } : {}),
178
+ additionalProperties: false,
179
+ },
180
+ outputSchema,
181
+ };
182
+ }
183
+ export function createMcpHandler(root, commandConfig = {}) {
184
+ const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
185
+ const allowedCommands = commandConfig.commands ?? [];
186
+ const subscriptions = new Set();
187
+ const notify = (event) => {
188
+ for (const subscription of subscriptions) {
189
+ const shouldNotify = event.type === "tools"
190
+ ? subscription.notifications.toolsListChanged
191
+ : event.type === "prompts"
192
+ ? subscription.notifications.promptsListChanged
193
+ : event.uri !== undefined
194
+ ? subscription.notifications.resourceSubscriptions.has(event.uri)
195
+ : subscription.notifications.resourcesListChanged;
196
+ if (shouldNotify) {
197
+ const method = event.type === "tools"
198
+ ? "notifications/tools/list_changed"
199
+ : event.type === "prompts"
200
+ ? "notifications/prompts/list_changed"
201
+ : event.uri !== undefined &&
202
+ subscription.notifications.resourceSubscriptions.has(event.uri)
203
+ ? "notifications/resources/updated"
204
+ : "notifications/resources/list_changed";
205
+ const params = event.uri === undefined ? undefined : { uri: event.uri };
206
+ writeSse(subscription.controller, {
207
+ jsonrpc: "2.0",
208
+ method,
209
+ ...(params ? { params } : {}),
210
+ _meta: { [SUBSCRIPTION_ID_META]: subscription.id },
211
+ });
212
+ }
213
+ }
214
+ };
215
+ const handler = async (request) => {
216
+ let message;
217
+ try {
218
+ message = (await request.json());
219
+ }
220
+ catch {
221
+ return jsonRpcError(null, -32700, "Parse error", 400);
222
+ }
223
+ if (message.jsonrpc !== "2.0" || typeof message.method !== "string")
224
+ return jsonRpcError(message.id ?? null, -32600, "Invalid Request", 400);
225
+ const hasId = Object.hasOwn(message, "id");
226
+ if (hasId && !isValidRequestId(message.id))
227
+ return jsonRpcError(null, -32600, "Invalid Request", 400);
228
+ if (!hasId)
229
+ return new Response(null, { status: 202 });
230
+ if (message.params !== undefined && !isRecord(message.params))
231
+ return jsonRpcError(message.id, -32602, "Invalid params.", 400);
232
+ const params = message.params ?? {};
233
+ if (!isRecord(params._meta))
234
+ return jsonRpcError(message.id ?? null, -32602, "Missing required request metadata.", 400);
235
+ const meta = params._meta;
236
+ const version = meta[PROTOCOL_META];
237
+ if (typeof version !== "string")
238
+ return jsonRpcError(message.id ?? null, -32602, "Missing required protocol version metadata.", 400);
239
+ if (version !== PROTOCOL_VERSION)
240
+ return jsonRpcError(message.id ?? null, -32022, "Unsupported protocol version.", 400, {
241
+ supported: [PROTOCOL_VERSION],
242
+ requested: version,
243
+ });
244
+ if (!isRecord(meta[CLIENT_CAPABILITIES_META]))
245
+ return jsonRpcError(message.id ?? null, -32602, "Missing required client capabilities metadata.", 400);
246
+ try {
247
+ switch (message.method) {
248
+ case "server/discover":
249
+ return json({
250
+ jsonrpc: "2.0",
251
+ id: message.id,
252
+ result: {
253
+ resultType: "complete",
254
+ supportedVersions: [PROTOCOL_VERSION],
255
+ capabilities: capabilities(),
256
+ _meta: SERVER_INFO_META,
257
+ instructions: "Expose and edit the project through secure filesystem tools, code intelligence, read-only Git inspection, project metadata, diagnostics, and project-local Agent Skills.",
258
+ ttlMs: 0,
259
+ cacheScope: "private",
260
+ },
261
+ });
262
+ case "tools/list": {
263
+ const page = paginate(baseTools, params.cursor);
264
+ return json({
265
+ jsonrpc: "2.0",
266
+ id: message.id,
267
+ result: {
268
+ resultType: "complete",
269
+ tools: page.items,
270
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
271
+ ttlMs: 0,
272
+ cacheScope: "private",
273
+ _meta: SERVER_INFO_META,
274
+ },
275
+ });
276
+ }
277
+ case "tools/call": {
278
+ if (typeof params.name !== "string" || params.name.length === 0)
279
+ return jsonRpcError(message.id ?? null, -32602, "tools/call name must be a non-empty string.", 400);
280
+ const toolName = params.name;
281
+ const toolDefinition = baseTools.find((item) => item.name === toolName);
282
+ if (!toolDefinition)
283
+ throw new Error(`Unknown tool: ${toolName}`);
284
+ if (params.arguments !== undefined && !isRecord(params.arguments))
285
+ return jsonRpcError(message.id ?? null, -32602, "tools/call arguments must be an object.", 400);
286
+ const argumentsValue = params.arguments ?? {};
287
+ const headerError = validateMcpParamHeaders(toolDefinition.inputSchema, argumentsValue, request.headers);
288
+ if (headerError)
289
+ return jsonRpcError(message.id ?? null, -32020, headerError, 400);
290
+ return json({
291
+ jsonrpc: "2.0",
292
+ id: message.id,
293
+ result: await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, notify, request.signal),
294
+ });
295
+ }
296
+ case "resources/list": {
297
+ const resources = (await listSkillResources(root)).map((item) => ({
298
+ ...item,
299
+ description: "Agent Skill resource",
300
+ }));
301
+ const page = paginate(resources, params.cursor);
302
+ return json({
303
+ jsonrpc: "2.0",
304
+ id: message.id,
305
+ result: {
306
+ resultType: "complete",
307
+ resources: page.items,
308
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
309
+ ttlMs: 0,
310
+ cacheScope: "private",
311
+ _meta: SERVER_INFO_META,
312
+ },
313
+ });
314
+ }
315
+ case "resources/read": {
316
+ if (typeof params.uri !== "string" || params.uri.length === 0)
317
+ return jsonRpcError(message.id ?? null, -32602, "resources/read uri must be a non-empty string.", 400);
318
+ const value = await readSkillResource(root, params.uri);
319
+ return json({
320
+ jsonrpc: "2.0",
321
+ id: message.id,
322
+ result: {
323
+ resultType: "complete",
324
+ contents: [value],
325
+ ttlMs: 0,
326
+ cacheScope: "private",
327
+ _meta: SERVER_INFO_META,
328
+ },
329
+ });
330
+ }
331
+ case "resources/directory/read": {
332
+ if (typeof params.uri !== "string" || params.uri.length === 0)
333
+ return jsonRpcError(message.id ?? null, -32602, "resources/directory/read uri must be a non-empty string.", 400);
334
+ const resources = await readSkillDirectory(root, params.uri);
335
+ const page = paginate(resources, params.cursor);
336
+ return json({
337
+ jsonrpc: "2.0",
338
+ id: message.id,
339
+ result: {
340
+ resultType: "complete",
341
+ resources: page.items,
342
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
343
+ ttlMs: 0,
344
+ cacheScope: "private",
345
+ _meta: SERVER_INFO_META,
346
+ },
347
+ });
348
+ }
349
+ case "skills/list": {
350
+ const page = paginate(await listSkills(root), params.cursor);
351
+ return json({
352
+ jsonrpc: "2.0",
353
+ id: message.id,
354
+ result: {
355
+ resultType: "complete",
356
+ skills: page.items,
357
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
358
+ ttlMs: 0,
359
+ cacheScope: "private",
360
+ _meta: SERVER_INFO_META,
361
+ },
362
+ });
363
+ }
364
+ case "subscriptions/listen": {
365
+ const notifications = subscriptionNotifications(params.notifications);
366
+ return subscriptionResponse(message.id, notifications, subscriptions);
367
+ }
368
+ case "skills/get":
369
+ if (typeof params.uri !== "string" || params.uri.length === 0)
370
+ return jsonRpcError(message.id ?? null, -32602, "skills/get uri must be a non-empty string.", 400);
371
+ return json({
372
+ jsonrpc: "2.0",
373
+ id: message.id,
374
+ result: {
375
+ resultType: "complete",
376
+ skill: await getSkill(root, params.uri),
377
+ ttlMs: 0,
378
+ cacheScope: "private",
379
+ _meta: SERVER_INFO_META,
380
+ },
381
+ });
382
+ default:
383
+ return jsonRpcError(message.id ?? null, -32601, `Method not found: ${message.method}`, 404);
384
+ }
385
+ }
386
+ catch (error) {
387
+ if (message.method === "tools/call")
388
+ return json({
389
+ jsonrpc: "2.0",
390
+ id: message.id,
391
+ result: {
392
+ resultType: "complete",
393
+ isError: true,
394
+ content: [
395
+ { type: "text", text: error instanceof Error ? error.message : String(error) },
396
+ ],
397
+ structuredContent: { error: error instanceof Error ? error.message : String(error) },
398
+ _meta: SERVER_INFO_META,
399
+ },
400
+ }, 200);
401
+ const code = message.method === "skills/get" ||
402
+ message.method === "resources/read" ||
403
+ message.method === "resources/directory/read" ||
404
+ message.method === "subscriptions/listen"
405
+ ? -32602
406
+ : -32603;
407
+ return jsonRpcError(message.id ?? null, code, error instanceof Error ? error.message : String(error), code === -32602 ? 400 : 500);
408
+ }
409
+ };
410
+ handler.closeSubscriptions = () => {
411
+ for (const subscription of subscriptions) {
412
+ writeSse(subscription.controller, {
413
+ jsonrpc: "2.0",
414
+ id: null,
415
+ result: {
416
+ resultType: "complete",
417
+ _meta: {
418
+ ...SERVER_INFO_META,
419
+ [SUBSCRIPTION_ID_META]: subscription.id,
420
+ },
421
+ },
422
+ });
423
+ subscription.controller.close();
424
+ }
425
+ subscriptions.clear();
426
+ };
427
+ return handler;
428
+ }
429
+ function capabilities() {
430
+ return {
431
+ tools: { listChanged: true },
432
+ resources: { listChanged: true, subscribe: true },
433
+ extensions: { "io.modelcontextprotocol/skills": { directoryRead: true } },
434
+ };
435
+ }
436
+ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, signal) {
437
+ let structuredContent;
438
+ switch (name) {
439
+ case "list_files":
440
+ structuredContent = await listFiles(root);
441
+ break;
442
+ case "list_dirs":
443
+ structuredContent = await listDirs(root, stringArray(args.paths, "paths"));
444
+ break;
445
+ case "read_range":
446
+ structuredContent = await readRange(root, String(args.path), Number(args.startLine), Number(args.endLine));
447
+ break;
448
+ case "read_files":
449
+ structuredContent = await Promise.all(stringArray(args.paths, "paths").map(async (p) => ({
450
+ path: p,
451
+ content: await readFile(root, p),
452
+ })));
453
+ break;
454
+ case "write_files": {
455
+ const output = [];
456
+ for (const file of objectArray(args.files, "files")) {
457
+ const p = String(file.path);
458
+ const before = await skillResourceUrisForPath(root, p);
459
+ await writeFile(root, p, String(file.content));
460
+ await notifyResourceChange(notify, root, p, before);
461
+ output.push({ path: p, written: true });
462
+ }
463
+ structuredContent = output;
464
+ break;
465
+ }
466
+ case "patch_files": {
467
+ const files = objectArray(args.files, "files").map((file) => ({
468
+ path: String(file.path),
469
+ patches: objectArray(file.patches, "patches").map((p) => ({
470
+ search: String(p.search),
471
+ replace: String(p.replace),
472
+ ...(p.count === undefined ? {} : { count: Number(p.count) }),
473
+ })),
474
+ }));
475
+ const before = new Map();
476
+ for (const file of files)
477
+ before.set(file.path, await skillResourceUrisForPath(root, file.path));
478
+ structuredContent = await patchFiles(root, files);
479
+ for (const file of files)
480
+ await notifyResourceChange(notify, root, file.path, before.get(file.path) ?? []);
481
+ break;
482
+ }
483
+ case "delete_files": {
484
+ const output = [];
485
+ for (const p of stringArray(args.paths, "paths")) {
486
+ const before = await skillResourceUrisForPath(root, p);
487
+ await deleteFile(root, p);
488
+ await notifyResourceChange(notify, root, p, before);
489
+ output.push({ path: p, deleted: true });
490
+ }
491
+ structuredContent = output;
492
+ break;
493
+ }
494
+ case "search_files":
495
+ structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000));
496
+ break;
497
+ case "find_files":
498
+ structuredContent = await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000));
499
+ break;
500
+ case "search_code":
501
+ structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000));
502
+ break;
503
+ case "find_symbol":
504
+ structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500));
505
+ break;
506
+ case "find_definition":
507
+ structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500));
508
+ break;
509
+ case "find_references":
510
+ structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000));
511
+ break;
512
+ case "find_imports":
513
+ structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000));
514
+ break;
515
+ case "find_exports":
516
+ structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000));
517
+ break;
518
+ case "git_status":
519
+ structuredContent = await gitStatus(root);
520
+ break;
521
+ case "git_stage":
522
+ structuredContent = await gitStage(root, stringArray(args.paths, "paths"));
523
+ break;
524
+ case "git_unstage":
525
+ structuredContent = await gitUnstage(root, stringArray(args.paths, "paths"));
526
+ break;
527
+ case "git_commit":
528
+ structuredContent = await gitCommit(root, String(args.message));
529
+ break;
530
+ case "git_restore":
531
+ structuredContent = await gitRestore(root, stringArray(args.paths, "paths"));
532
+ break;
533
+ case "git_push":
534
+ structuredContent = await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch));
535
+ break;
536
+ case "git_diff":
537
+ structuredContent = await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path));
538
+ break;
539
+ case "git_log":
540
+ structuredContent = await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path));
541
+ break;
542
+ case "project_overview":
543
+ structuredContent = await projectOverview(root);
544
+ break;
545
+ case "package_info":
546
+ structuredContent = await packageInfo(root);
547
+ break;
548
+ case "file_info":
549
+ structuredContent = await fileInfo(root, String(args.path));
550
+ break;
551
+ case "diagnostics":
552
+ structuredContent = await diagnostics(root, numberArg(args.maxResults, 200, 1000));
553
+ break;
554
+ case "run_npm_batch": {
555
+ const output = [];
556
+ for (const script of stringArray(args.scripts, "scripts")) {
557
+ const value = await runNpm(root, script, 120000, npmAllowed, signal);
558
+ output.push(value);
559
+ if (value.exitCode !== 0)
560
+ break;
561
+ }
562
+ structuredContent = output;
563
+ break;
564
+ }
565
+ case "run_command_batch": {
566
+ const output = [];
567
+ for (const item of objectArray(args.commands, "commands")) {
568
+ const value = await runCommand(root, String(item.command), Array.isArray(item.args) ? item.args.map(String) : [], allowedCommands, 120000, signal);
569
+ output.push(value);
570
+ if (value.exitCode !== 0)
571
+ break;
572
+ }
573
+ structuredContent = output;
574
+ break;
575
+ }
576
+ default:
577
+ throw new Error(`Unknown tool: ${name}`);
578
+ }
579
+ return completeToolResult(structuredContent);
580
+ }
581
+ const MCP_PARAM_PREFIX = "Mcp-Param-";
582
+ const X_MCP_HEADER = "x-mcp-header";
583
+ export function validateMcpParamHeaders(inputSchema, args, headers) {
584
+ const annotations = collectMcpHeaderAnnotations(inputSchema);
585
+ for (const annotation of annotations) {
586
+ const value = valueAtPath(args, annotation.path);
587
+ const headerName = `${MCP_PARAM_PREFIX}${annotation.headerName}`;
588
+ const headerValue = headers.get(headerName);
589
+ if (value === undefined || value === null) {
590
+ if (headerValue !== null)
591
+ return `${headerName} must be absent when the annotated argument is absent.`;
592
+ continue;
593
+ }
594
+ const rendered = renderMcpHeaderValue(value);
595
+ if (rendered === undefined)
596
+ return `${headerName} can only mirror string, integer, or boolean arguments.`;
597
+ if (headerValue === null)
598
+ return `${headerName} is required for the annotated argument.`;
599
+ const decoded = decodeMcpHeaderValue(headerValue);
600
+ if (decoded === undefined || !mcpHeaderValuesEqual(annotation.type, rendered, decoded))
601
+ return `${headerName} does not match the annotated argument.`;
602
+ }
603
+ return undefined;
604
+ }
605
+ function collectMcpHeaderAnnotations(schema) {
606
+ const result = [];
607
+ const visit = (node, path) => {
608
+ if (typeof node !== "object" || node === null || Array.isArray(node))
609
+ return;
610
+ const record = node;
611
+ const properties = record.properties;
612
+ if (typeof properties !== "object" || properties === null || Array.isArray(properties))
613
+ return;
614
+ for (const [name, child] of Object.entries(properties)) {
615
+ if (typeof child !== "object" || child === null || Array.isArray(child))
616
+ continue;
617
+ const property = child;
618
+ if (property[X_MCP_HEADER] !== undefined) {
619
+ const headerName = property[X_MCP_HEADER];
620
+ if (typeof headerName !== "string" ||
621
+ !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(headerName) ||
622
+ !headerName) {
623
+ throw new Error(`Invalid x-mcp-header annotation for property ${[...path, name].join(".")}.`);
624
+ }
625
+ if (property.type !== "string" &&
626
+ property.type !== "integer" &&
627
+ property.type !== "boolean")
628
+ throw new Error(`Invalid x-mcp-header type for property ${[...path, name].join(".")}.`);
629
+ if (result.some((item) => item.headerName.toLowerCase() === headerName.toLowerCase()))
630
+ throw new Error(`Duplicate x-mcp-header annotation: ${headerName}.`);
631
+ result.push({
632
+ path: [...path, name],
633
+ headerName,
634
+ type: property.type,
635
+ });
636
+ }
637
+ visit(property, [...path, name]);
638
+ }
639
+ };
640
+ visit(schema, []);
641
+ return result;
642
+ }
643
+ function valueAtPath(value, path) {
644
+ let current = value;
645
+ for (const key of path) {
646
+ if (typeof current !== "object" || current === null || Array.isArray(current))
647
+ return undefined;
648
+ current = current[key];
649
+ }
650
+ return current;
651
+ }
652
+ function renderMcpHeaderValue(value) {
653
+ if (typeof value === "string")
654
+ return value;
655
+ if (typeof value === "boolean")
656
+ return value ? "true" : "false";
657
+ if (typeof value === "number" && Number.isInteger(value))
658
+ return String(value);
659
+ return undefined;
660
+ }
661
+ function decodeMcpHeaderValue(value) {
662
+ const prefix = "=?base64?";
663
+ const suffix = "?=";
664
+ if (!value.startsWith(prefix) || !value.endsWith(suffix))
665
+ return value;
666
+ try {
667
+ const encoded = value.slice(prefix.length, -suffix.length);
668
+ const decoded = Buffer.from(encoded, "base64").toString("utf8");
669
+ if (Buffer.from(decoded, "utf8").toString("base64") !== encoded)
670
+ return undefined;
671
+ return decoded;
672
+ }
673
+ catch {
674
+ return undefined;
675
+ }
676
+ }
677
+ function mcpHeaderValuesEqual(type, bodyValue, headerValue) {
678
+ if (type === "integer")
679
+ return Number.isSafeInteger(Number(headerValue)) && Number(headerValue) === Number(bodyValue);
680
+ return bodyValue === headerValue;
681
+ }
682
+ function completeToolResult(structuredContent) {
683
+ return {
684
+ resultType: "complete",
685
+ content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
686
+ structuredContent,
687
+ _meta: SERVER_INFO_META,
688
+ };
689
+ }
690
+ function isRecord(value) {
691
+ return typeof value === "object" && value !== null && !Array.isArray(value);
692
+ }
693
+ function isValidRequestId(value) {
694
+ return typeof value === "string" || (typeof value === "number" && Number.isSafeInteger(value));
695
+ }
696
+ function stringArray(value, name) {
697
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
698
+ throw new Error(`${name} must be an array of strings.`);
699
+ return value;
700
+ }
701
+ function objectArray(value, name) {
702
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "object" || item === null))
703
+ throw new Error(`${name} must be an array of objects.`);
704
+ return value;
705
+ }
706
+ function numberArg(value, fallback, max) {
707
+ const result = value === undefined ? fallback : Number(value);
708
+ if (!Number.isInteger(result) || result < 1 || result > max)
709
+ throw new Error(`Value must be an integer between 1 and ${max}.`);
710
+ return result;
711
+ }
712
+ function jsonRpcError(id, code, message, status, data) {
713
+ return json({ jsonrpc: "2.0", id, error: { code, message, ...(data === undefined ? {} : { data }) } }, status);
714
+ }
715
+ function paginate(items, cursorValue) {
716
+ const offset = decodeCursor(cursorValue);
717
+ const page = items.slice(offset, offset + PAGE_SIZE);
718
+ const nextOffset = offset + page.length;
719
+ return nextOffset < items.length
720
+ ? { items: page, nextCursor: encodeCursor(nextOffset) }
721
+ : { items: page };
722
+ }
723
+ function encodeCursor(offset) {
724
+ return Buffer.from(String(offset), "utf8").toString("base64url");
725
+ }
726
+ function decodeCursor(value) {
727
+ if (value === undefined)
728
+ return 0;
729
+ if (typeof value !== "string" || value.length > 128)
730
+ throw new Error("Invalid pagination cursor.");
731
+ let decoded;
732
+ try {
733
+ decoded = Buffer.from(value, "base64url").toString("utf8");
734
+ }
735
+ catch {
736
+ throw new Error("Invalid pagination cursor.");
737
+ }
738
+ const offset = Number(decoded);
739
+ if (!Number.isSafeInteger(offset) || offset < 0)
740
+ throw new Error("Invalid pagination cursor.");
741
+ return offset;
742
+ }
743
+ async function notifyResourceChange(notify, root, pathValue, before) {
744
+ const after = await skillResourceUrisForPath(root, pathValue);
745
+ if (before.length === 0 && after.length === 0)
746
+ return;
747
+ if (before.length === 0 || after.length === 0) {
748
+ notify({ type: "resources" });
749
+ return;
750
+ }
751
+ for (const uri of after)
752
+ notify({ type: "resources", uri });
753
+ }
754
+ function subscriptionNotifications(value) {
755
+ if (!isRecord(value))
756
+ throw new Error("subscriptions/listen notifications are required.");
757
+ for (const key of ["toolsListChanged", "promptsListChanged", "resourcesListChanged"]) {
758
+ if (value[key] !== undefined && typeof value[key] !== "boolean")
759
+ throw new Error(`subscriptions/listen ${key} must be a boolean.`);
760
+ }
761
+ if (value.resourceSubscriptions !== undefined &&
762
+ (!Array.isArray(value.resourceSubscriptions) ||
763
+ value.resourceSubscriptions.some((uri) => typeof uri !== "string" || uri.length === 0)))
764
+ throw new Error("subscriptions/listen resourceSubscriptions must be an array of non-empty strings.");
765
+ const resourceSubscriptions = value.resourceSubscriptions === undefined ? [] : value.resourceSubscriptions;
766
+ return {
767
+ toolsListChanged: value.toolsListChanged === true,
768
+ promptsListChanged: value.promptsListChanged === true,
769
+ resourcesListChanged: value.resourcesListChanged === true,
770
+ resourceSubscriptions: new Set(resourceSubscriptions),
771
+ };
772
+ }
773
+ function subscriptionResponse(id, notifications, subscriptions) {
774
+ let subscription;
775
+ const stream = new ReadableStream({
776
+ start(controller) {
777
+ const honored = {
778
+ toolsListChanged: notifications.toolsListChanged,
779
+ promptsListChanged: notifications.promptsListChanged,
780
+ resourcesListChanged: notifications.resourcesListChanged,
781
+ resourceSubscriptions: notifications.resourceSubscriptions,
782
+ };
783
+ subscription = { id, controller, notifications: honored };
784
+ subscriptions.add(subscription);
785
+ writeSse(controller, {
786
+ jsonrpc: "2.0",
787
+ method: "notifications/subscriptions/acknowledged",
788
+ params: {
789
+ notifications: {
790
+ ...(honored.toolsListChanged ? { toolsListChanged: true } : {}),
791
+ ...(honored.promptsListChanged ? { promptsListChanged: true } : {}),
792
+ ...(honored.resourcesListChanged ? { resourcesListChanged: true } : {}),
793
+ ...(honored.resourceSubscriptions.size
794
+ ? { resourceSubscriptions: [...honored.resourceSubscriptions] }
795
+ : {}),
796
+ },
797
+ },
798
+ _meta: { ...SERVER_INFO_META, [SUBSCRIPTION_ID_META]: id },
799
+ });
800
+ },
801
+ cancel() {
802
+ if (subscription)
803
+ subscriptions.delete(subscription);
804
+ },
805
+ });
806
+ return new Response(stream, {
807
+ status: 200,
808
+ headers: {
809
+ "content-type": "text/event-stream",
810
+ "cache-control": "no-cache",
811
+ connection: "keep-alive",
812
+ },
813
+ });
814
+ }
815
+ function writeSse(controller, message) {
816
+ controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(message)}\\n\\n`));
817
+ }
818
+ function json(value, status = 200) {
819
+ return new Response(JSON.stringify(value), {
820
+ status,
821
+ headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
822
+ });
823
+ }
824
+ //# sourceMappingURL=mcp.js.map