agent-dir 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -5
- package/dist/bin/agent-dir.js +261 -10
- 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/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 +29 -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 +348 -39
- 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/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/types.d.ts +7 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +5 -2
package/dist/src/mcp.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import { CodeGraphIntegration } from "./codegraph.js";
|
|
4
|
+
import { TelemetryRecorder } from "./telemetry/recorder.js";
|
|
3
5
|
import { findDefinition, findExports, findFiles, findImports, findReferences, findSymbol, searchCode, searchFiles, } from "./tools/code.js";
|
|
4
6
|
import { runCommand } from "./tools/commands.js";
|
|
5
7
|
import { diagnostics } from "./tools/diagnostics.js";
|
|
@@ -327,10 +329,28 @@ function tool(name, description, properties, outputSchema, required = []) {
|
|
|
327
329
|
outputSchema,
|
|
328
330
|
};
|
|
329
331
|
}
|
|
330
|
-
export function createMcpHandler(root, commandConfig = {}) {
|
|
332
|
+
export function createMcpHandler(root, commandConfig = {}, telemetry = { level: "none" }) {
|
|
331
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;
|
|
332
350
|
const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
|
|
333
351
|
const allowedCommands = commandConfig.commands ?? [];
|
|
352
|
+
const blacklistedCommands = commandConfig.blacklistedCommands ?? [];
|
|
353
|
+
const gitEnabled = commandConfig.git ?? allowedCommands.includes("git");
|
|
334
354
|
const subscriptions = new Set();
|
|
335
355
|
const notify = (event) => {
|
|
336
356
|
for (const subscription of subscriptions) {
|
|
@@ -361,23 +381,65 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
361
381
|
}
|
|
362
382
|
};
|
|
363
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;
|
|
364
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
|
+
};
|
|
365
404
|
try {
|
|
366
|
-
|
|
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;
|
|
367
412
|
}
|
|
368
413
|
catch {
|
|
414
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
415
|
+
recordMcpRequest("unknown");
|
|
369
416
|
return jsonRpcError(null, -32700, "Parse error", 400);
|
|
370
417
|
}
|
|
371
|
-
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");
|
|
372
421
|
return jsonRpcError(message.id ?? null, -32600, "Invalid Request", 400);
|
|
422
|
+
}
|
|
373
423
|
const hasId = Object.hasOwn(message, "id");
|
|
374
|
-
if (hasId && !isValidRequestId(message.id))
|
|
424
|
+
if (hasId && !isValidRequestId(message.id)) {
|
|
425
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
426
|
+
recordMcpRequest(message.method);
|
|
375
427
|
return jsonRpcError(null, -32600, "Invalid Request", 400);
|
|
376
|
-
|
|
428
|
+
}
|
|
429
|
+
if (!hasId) {
|
|
430
|
+
mcpSuccess = message.method.startsWith("notifications/");
|
|
431
|
+
if (!mcpSuccess)
|
|
432
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
433
|
+
recordMcpRequest();
|
|
377
434
|
return new Response(null, { status: 202 });
|
|
378
|
-
|
|
435
|
+
}
|
|
436
|
+
if (message.params !== undefined && !isRecord(message.params)) {
|
|
437
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
438
|
+
recordMcpRequest();
|
|
379
439
|
return jsonRpcError(message.id, -32602, "Invalid params.", 400);
|
|
440
|
+
}
|
|
380
441
|
const params = message.params ?? {};
|
|
442
|
+
runtime = telemetryRuntime(params);
|
|
381
443
|
const requestedProtocolVersion = typeof params.protocolVersion === "string" ? params.protocolVersion : undefined;
|
|
382
444
|
const legacyInitialize = message.method === "initialize";
|
|
383
445
|
const legacyRequest = !legacyInitialize &&
|
|
@@ -385,15 +447,26 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
385
447
|
!isRecord(params._meta);
|
|
386
448
|
if (legacyInitialize) {
|
|
387
449
|
const protocolHeader = request.headers.get("mcp-protocol-version");
|
|
388
|
-
if (protocolHeader !== null && protocolHeader !== LEGACY_PROTOCOL_VERSION)
|
|
450
|
+
if (protocolHeader !== null && protocolHeader !== LEGACY_PROTOCOL_VERSION) {
|
|
451
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
452
|
+
recordMcpRequest();
|
|
389
453
|
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version does not match the initialize protocol version.", 400);
|
|
390
|
-
|
|
454
|
+
}
|
|
455
|
+
if (requestedProtocolVersion !== LEGACY_PROTOCOL_VERSION) {
|
|
456
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
457
|
+
recordMcpRequest();
|
|
391
458
|
return jsonRpcError(message.id ?? null, -32602, "Unsupported protocol version.", 400, {
|
|
392
459
|
supported: [LEGACY_PROTOCOL_VERSION],
|
|
393
460
|
requested: requestedProtocolVersion,
|
|
394
461
|
});
|
|
395
|
-
|
|
462
|
+
}
|
|
463
|
+
if (!isRecord(params.capabilities) || !isRecord(params.clientInfo)) {
|
|
464
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
465
|
+
recordMcpRequest();
|
|
396
466
|
return jsonRpcError(message.id ?? null, -32602, "Invalid initialize params.", 400);
|
|
467
|
+
}
|
|
468
|
+
mcpSuccess = true;
|
|
469
|
+
recordMcpRequest();
|
|
397
470
|
return json({
|
|
398
471
|
jsonrpc: "2.0",
|
|
399
472
|
id: message.id,
|
|
@@ -401,30 +474,46 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
401
474
|
protocolVersion: LEGACY_PROTOCOL_VERSION,
|
|
402
475
|
capabilities: capabilities(),
|
|
403
476
|
serverInfo: SERVER_INFO,
|
|
404
|
-
instructions: buildAgentInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
477
|
+
instructions: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
405
478
|
},
|
|
406
479
|
});
|
|
407
480
|
}
|
|
408
481
|
if (legacyRequest) {
|
|
409
|
-
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();
|
|
410
485
|
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version must be 2025-11-25 for legacy requests.", 400);
|
|
486
|
+
}
|
|
411
487
|
}
|
|
412
|
-
else if (!isRecord(params._meta))
|
|
488
|
+
else if (!isRecord(params._meta)) {
|
|
489
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
490
|
+
recordMcpRequest();
|
|
413
491
|
return jsonRpcError(message.id ?? null, -32602, "Missing required request metadata.", 400);
|
|
492
|
+
}
|
|
414
493
|
const meta = isRecord(params._meta) ? params._meta : {};
|
|
415
494
|
const version = meta[PROTOCOL_META];
|
|
416
|
-
if (!legacyRequest && typeof version !== "string")
|
|
495
|
+
if (!legacyRequest && typeof version !== "string") {
|
|
496
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
497
|
+
recordMcpRequest();
|
|
417
498
|
return jsonRpcError(message.id ?? null, -32602, "Missing required protocol version metadata.", 400);
|
|
418
|
-
|
|
499
|
+
}
|
|
500
|
+
if (!legacyRequest && version !== PROTOCOL_VERSION) {
|
|
501
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
502
|
+
recordMcpRequest();
|
|
419
503
|
return jsonRpcError(message.id ?? null, -32022, "Unsupported protocol version.", 400, {
|
|
420
504
|
supported: [PROTOCOL_VERSION],
|
|
421
505
|
requested: version,
|
|
422
506
|
});
|
|
423
|
-
|
|
507
|
+
}
|
|
508
|
+
if (!legacyRequest && !isRecord(meta[CLIENT_CAPABILITIES_META])) {
|
|
509
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
510
|
+
recordMcpRequest();
|
|
424
511
|
return jsonRpcError(message.id ?? null, -32602, "Missing required client capabilities metadata.", 400);
|
|
512
|
+
}
|
|
425
513
|
try {
|
|
426
514
|
switch (message.method) {
|
|
427
515
|
case "server/discover":
|
|
516
|
+
mcpSuccess = true;
|
|
428
517
|
return json({
|
|
429
518
|
jsonrpc: "2.0",
|
|
430
519
|
id: message.id,
|
|
@@ -433,14 +522,15 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
433
522
|
supportedVersions: [PROTOCOL_VERSION],
|
|
434
523
|
capabilities: capabilities(),
|
|
435
524
|
_meta: SERVER_INFO_META,
|
|
436
|
-
instructions: buildAgentInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
525
|
+
instructions: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
437
526
|
codeGraph: await codeGraph.capability(),
|
|
438
527
|
ttlMs: 0,
|
|
439
528
|
cacheScope: "private",
|
|
440
529
|
},
|
|
441
530
|
});
|
|
442
531
|
case "tools/list": {
|
|
443
|
-
const page = paginate(await availableTools(codeGraph), params.cursor);
|
|
532
|
+
const page = paginate(await availableTools(codeGraph, gitEnabled), params.cursor);
|
|
533
|
+
mcpSuccess = true;
|
|
444
534
|
return json({
|
|
445
535
|
jsonrpc: "2.0",
|
|
446
536
|
id: message.id,
|
|
@@ -455,23 +545,86 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
455
545
|
});
|
|
456
546
|
}
|
|
457
547
|
case "tools/call": {
|
|
458
|
-
if (typeof params.name !== "string" || params.name.length === 0)
|
|
548
|
+
if (typeof params.name !== "string" || params.name.length === 0) {
|
|
549
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
459
550
|
return jsonRpcError(message.id ?? null, -32602, "tools/call name must be a non-empty string.", 400);
|
|
551
|
+
}
|
|
460
552
|
const toolName = params.name;
|
|
461
|
-
const toolDefinition = (await availableTools(codeGraph)).find((item) => item.name === toolName);
|
|
553
|
+
const toolDefinition = (await availableTools(codeGraph, gitEnabled)).find((item) => item.name === toolName);
|
|
462
554
|
if (!toolDefinition)
|
|
463
555
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
464
|
-
if (params.arguments !== undefined && !isRecord(params.arguments))
|
|
556
|
+
if (params.arguments !== undefined && !isRecord(params.arguments)) {
|
|
557
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
465
558
|
return jsonRpcError(message.id ?? null, -32602, "tools/call arguments must be an object.", 400);
|
|
559
|
+
}
|
|
466
560
|
const argumentsValue = params.arguments ?? {};
|
|
467
561
|
const headerError = validateMcpParamHeaders(toolDefinition.inputSchema, argumentsValue, request.headers);
|
|
468
|
-
if (headerError)
|
|
562
|
+
if (headerError) {
|
|
563
|
+
telemetryRequestErrorCategory = "invalid_input";
|
|
469
564
|
return jsonRpcError(message.id ?? null, -32020, headerError, 400);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
}
|
|
475
628
|
}
|
|
476
629
|
case "resources/list": {
|
|
477
630
|
const resources = [
|
|
@@ -493,6 +646,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
493
646
|
})),
|
|
494
647
|
];
|
|
495
648
|
const page = paginate(resources, params.cursor);
|
|
649
|
+
mcpSuccess = true;
|
|
496
650
|
return json({
|
|
497
651
|
jsonrpc: "2.0",
|
|
498
652
|
id: message.id,
|
|
@@ -509,7 +663,8 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
509
663
|
case "resources/read": {
|
|
510
664
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
511
665
|
return jsonRpcError(message.id ?? null, -32602, "resources/read uri must be a non-empty string.", 400);
|
|
512
|
-
if (params.uri === INSTRUCTIONS_URI)
|
|
666
|
+
if (params.uri === INSTRUCTIONS_URI) {
|
|
667
|
+
mcpSuccess = true;
|
|
513
668
|
return json({
|
|
514
669
|
jsonrpc: "2.0",
|
|
515
670
|
id: message.id,
|
|
@@ -519,7 +674,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
519
674
|
{
|
|
520
675
|
uri: INSTRUCTIONS_URI,
|
|
521
676
|
mimeType: "text/markdown",
|
|
522
|
-
text: buildAgentInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
677
|
+
text: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
523
678
|
},
|
|
524
679
|
],
|
|
525
680
|
ttlMs: 0,
|
|
@@ -527,7 +682,9 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
527
682
|
_meta: SERVER_INFO_META,
|
|
528
683
|
},
|
|
529
684
|
});
|
|
530
|
-
|
|
685
|
+
}
|
|
686
|
+
if (params.uri === CAPABILITIES_URI) {
|
|
687
|
+
mcpSuccess = true;
|
|
531
688
|
return json({
|
|
532
689
|
jsonrpc: "2.0",
|
|
533
690
|
id: message.id,
|
|
@@ -537,7 +694,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
537
694
|
{
|
|
538
695
|
uri: CAPABILITIES_URI,
|
|
539
696
|
mimeType: "application/json",
|
|
540
|
-
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands), null, 2),
|
|
697
|
+
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled), null, 2),
|
|
541
698
|
},
|
|
542
699
|
],
|
|
543
700
|
ttlMs: 0,
|
|
@@ -545,7 +702,9 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
545
702
|
_meta: SERVER_INFO_META,
|
|
546
703
|
},
|
|
547
704
|
});
|
|
705
|
+
}
|
|
548
706
|
const value = await readSkillResource(root, params.uri);
|
|
707
|
+
mcpSuccess = true;
|
|
549
708
|
return json({
|
|
550
709
|
jsonrpc: "2.0",
|
|
551
710
|
id: message.id,
|
|
@@ -563,6 +722,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
563
722
|
return jsonRpcError(message.id ?? null, -32602, "resources/directory/read uri must be a non-empty string.", 400);
|
|
564
723
|
const resources = await readSkillDirectory(root, params.uri);
|
|
565
724
|
const page = paginate(resources, params.cursor);
|
|
725
|
+
mcpSuccess = true;
|
|
566
726
|
return json({
|
|
567
727
|
jsonrpc: "2.0",
|
|
568
728
|
id: message.id,
|
|
@@ -578,6 +738,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
578
738
|
}
|
|
579
739
|
case "skills/list": {
|
|
580
740
|
const page = paginate(await listSkills(root), params.cursor);
|
|
741
|
+
mcpSuccess = true;
|
|
581
742
|
return json({
|
|
582
743
|
jsonrpc: "2.0",
|
|
583
744
|
id: message.id,
|
|
@@ -593,11 +754,13 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
593
754
|
}
|
|
594
755
|
case "subscriptions/listen": {
|
|
595
756
|
const notifications = subscriptionNotifications(params.notifications);
|
|
757
|
+
mcpSuccess = true;
|
|
596
758
|
return subscriptionResponse(message.id, notifications, subscriptions);
|
|
597
759
|
}
|
|
598
760
|
case "skills/get":
|
|
599
761
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
600
762
|
return jsonRpcError(message.id ?? null, -32602, "skills/get uri must be a non-empty string.", 400);
|
|
763
|
+
mcpSuccess = true;
|
|
601
764
|
return json({
|
|
602
765
|
jsonrpc: "2.0",
|
|
603
766
|
id: message.id,
|
|
@@ -610,10 +773,12 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
610
773
|
},
|
|
611
774
|
});
|
|
612
775
|
default:
|
|
776
|
+
telemetryRequestErrorCategory = "not_found";
|
|
613
777
|
return jsonRpcError(message.id ?? null, -32601, `Method not found: ${message.method}`, 404);
|
|
614
778
|
}
|
|
615
779
|
}
|
|
616
780
|
catch (error) {
|
|
781
|
+
telemetryRequestErrorCategory = classifyTelemetryError(error);
|
|
617
782
|
if (message.method === "tools/call")
|
|
618
783
|
return json({
|
|
619
784
|
jsonrpc: "2.0",
|
|
@@ -637,8 +802,29 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
637
802
|
: -32603;
|
|
638
803
|
return jsonRpcError(message.id ?? null, code, error instanceof Error ? error.message : String(error), code === -32602 ? 400 : 500);
|
|
639
804
|
}
|
|
805
|
+
finally {
|
|
806
|
+
const requestDurationMs = performance.now() - requestStartedAt;
|
|
807
|
+
sessionActiveRequestDurationMs += requestDurationMs;
|
|
808
|
+
sessionLastActivityAt = performance.now();
|
|
809
|
+
recordMcpRequest();
|
|
810
|
+
}
|
|
640
811
|
};
|
|
812
|
+
handler.telemetrySnapshot = () => telemetryRecorder.snapshot();
|
|
641
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
|
+
}
|
|
642
828
|
for (const subscription of subscriptions) {
|
|
643
829
|
writeSse(subscription.controller, {
|
|
644
830
|
jsonrpc: "2.0",
|
|
@@ -658,19 +844,23 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
658
844
|
};
|
|
659
845
|
return handler;
|
|
660
846
|
}
|
|
661
|
-
async function availableTools(codeGraph) {
|
|
847
|
+
async function availableTools(codeGraph, gitEnabled) {
|
|
662
848
|
const capability = await codeGraph.capability();
|
|
663
|
-
|
|
849
|
+
const tools = capability.status === "available" ||
|
|
664
850
|
capability.status === "startup_failed" ||
|
|
665
851
|
capability.status === "runtime_failed"
|
|
666
852
|
? [...baseTools, codeGraphTool]
|
|
667
853
|
: baseTools;
|
|
854
|
+
return gitEnabled ? tools : tools.filter((item) => !item.name.startsWith("git_"));
|
|
668
855
|
}
|
|
669
|
-
function buildAgentInstructions(npmAllowed, allowedCommands, codeGraph) {
|
|
856
|
+
function buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, codeGraph) {
|
|
670
857
|
const npm = npmAllowed.length ? npmAllowed.map((item) => `- ${item}`).join("\n") : "- none";
|
|
671
858
|
const commands = allowedCommands.length
|
|
672
859
|
? allowedCommands.map((item) => `- ${item}`).join("\n")
|
|
673
860
|
: "- none";
|
|
861
|
+
const blacklist = blacklistedCommands.length
|
|
862
|
+
? blacklistedCommands.map((item) => `- ${item}`).join("\n")
|
|
863
|
+
: "- none";
|
|
674
864
|
const codeGraphSection = codeGraph.status === "available" ||
|
|
675
865
|
codeGraph.status === "startup_failed" ||
|
|
676
866
|
codeGraph.status === "runtime_failed"
|
|
@@ -679,14 +869,21 @@ function buildAgentInstructions(npmAllowed, allowedCommands, codeGraph) {
|
|
|
679
869
|
(codeGraph.detail ? ` — ${codeGraph.detail}` : "") +
|
|
680
870
|
"."
|
|
681
871
|
: "";
|
|
682
|
-
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\nOnly use commands from the execution policy. Do not attempt to bypass it with shells or alternate executables.\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}`;
|
|
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}`;
|
|
683
873
|
}
|
|
684
|
-
function buildAgentCapabilities(npmAllowed, allowedCommands) {
|
|
874
|
+
function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled) {
|
|
685
875
|
return {
|
|
686
876
|
version: packageVersion,
|
|
687
|
-
tools: baseTools
|
|
877
|
+
tools: baseTools
|
|
878
|
+
.filter((item) => gitEnabled || !item.name.startsWith("git_"))
|
|
879
|
+
.map((item) => item.name),
|
|
688
880
|
resources: [INSTRUCTIONS_URI, CAPABILITIES_URI],
|
|
689
|
-
execution: {
|
|
881
|
+
execution: {
|
|
882
|
+
npmScripts: [...npmAllowed],
|
|
883
|
+
commands: [...allowedCommands],
|
|
884
|
+
blacklistedCommands: [...blacklistedCommands],
|
|
885
|
+
git: gitEnabled,
|
|
886
|
+
},
|
|
690
887
|
efficiency: {
|
|
691
888
|
preferred: {
|
|
692
889
|
orientation: "project_context",
|
|
@@ -716,7 +913,7 @@ function capabilities() {
|
|
|
716
913
|
extensions: { "io.modelcontextprotocol/skills": { directoryRead: true } },
|
|
717
914
|
};
|
|
718
915
|
}
|
|
719
|
-
async function callTool(name, args, root, npmAllowed, allowedCommands, notify, signal, codeGraph) {
|
|
916
|
+
async function callTool(name, args, root, npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, notify, signal, codeGraph) {
|
|
720
917
|
let structuredContent;
|
|
721
918
|
if (name === "codegraph_explore") {
|
|
722
919
|
if (!codeGraph)
|
|
@@ -728,6 +925,9 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
728
925
|
throw new Error("maxFiles must be an integer between 1 and 100.");
|
|
729
926
|
return await codeGraph.explore(args.query, args.maxFiles === undefined ? undefined : Number(args.maxFiles));
|
|
730
927
|
}
|
|
928
|
+
if (name.startsWith("git_") && !gitEnabled) {
|
|
929
|
+
throw new Error("Dedicated Git tools are disabled for this profile.");
|
|
930
|
+
}
|
|
731
931
|
switch (name) {
|
|
732
932
|
case "list_files": {
|
|
733
933
|
const items = await listFiles(root);
|
|
@@ -1158,6 +1358,7 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
1158
1358
|
structuredContent = {
|
|
1159
1359
|
npmScripts: [...npmAllowed],
|
|
1160
1360
|
commands: [...allowedCommands],
|
|
1361
|
+
blacklistedCommands: [...blacklistedCommands],
|
|
1161
1362
|
};
|
|
1162
1363
|
break;
|
|
1163
1364
|
case "file_info":
|
|
@@ -1197,7 +1398,7 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
1197
1398
|
case "run_command_batch": {
|
|
1198
1399
|
const output = [];
|
|
1199
1400
|
for (const item of objectArray(args.commands, "commands")) {
|
|
1200
|
-
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);
|
|
1201
1402
|
output.push(value);
|
|
1202
1403
|
if (value.exitCode !== 0)
|
|
1203
1404
|
break;
|
|
@@ -1336,6 +1537,114 @@ function completeToolResult(structuredContent) {
|
|
|
1336
1537
|
_meta: SERVER_INFO_META,
|
|
1337
1538
|
};
|
|
1338
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
|
+
}
|
|
1339
1648
|
function isRecord(value) {
|
|
1340
1649
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1341
1650
|
}
|