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.
Files changed (56) hide show
  1. package/README.md +116 -5
  2. package/dist/bin/agent-dir.js +261 -10
  3. package/dist/bin/agent-dir.js.map +1 -1
  4. package/dist/src/auth.d.ts.map +1 -1
  5. package/dist/src/auth.js +10 -2
  6. package/dist/src/auth.js.map +1 -1
  7. package/dist/src/config.d.ts.map +1 -1
  8. package/dist/src/config.js +14 -0
  9. package/dist/src/config.js.map +1 -1
  10. package/dist/src/logging.d.ts +2 -0
  11. package/dist/src/logging.d.ts.map +1 -1
  12. package/dist/src/logging.js +29 -10
  13. package/dist/src/logging.js.map +1 -1
  14. package/dist/src/mcp.d.ts +4 -1
  15. package/dist/src/mcp.d.ts.map +1 -1
  16. package/dist/src/mcp.js +348 -39
  17. package/dist/src/mcp.js.map +1 -1
  18. package/dist/src/server.d.ts +1 -1
  19. package/dist/src/server.d.ts.map +1 -1
  20. package/dist/src/server.js +7 -10
  21. package/dist/src/server.js.map +1 -1
  22. package/dist/src/setup.d.ts +9 -0
  23. package/dist/src/setup.d.ts.map +1 -0
  24. package/dist/src/setup.js +215 -0
  25. package/dist/src/setup.js.map +1 -0
  26. package/dist/src/telemetry/aggregate.d.ts +58 -0
  27. package/dist/src/telemetry/aggregate.d.ts.map +1 -0
  28. package/dist/src/telemetry/aggregate.js +184 -0
  29. package/dist/src/telemetry/aggregate.js.map +1 -0
  30. package/dist/src/telemetry/config.d.ts +5 -0
  31. package/dist/src/telemetry/config.d.ts.map +1 -0
  32. package/dist/src/telemetry/config.js +16 -0
  33. package/dist/src/telemetry/config.js.map +1 -0
  34. package/dist/src/telemetry/format.d.ts +3 -0
  35. package/dist/src/telemetry/format.d.ts.map +1 -0
  36. package/dist/src/telemetry/format.js +87 -0
  37. package/dist/src/telemetry/format.js.map +1 -0
  38. package/dist/src/telemetry/privacy.d.ts +4 -0
  39. package/dist/src/telemetry/privacy.d.ts.map +1 -0
  40. package/dist/src/telemetry/privacy.js +145 -0
  41. package/dist/src/telemetry/privacy.js.map +1 -0
  42. package/dist/src/telemetry/recorder.d.ts +30 -0
  43. package/dist/src/telemetry/recorder.d.ts.map +1 -0
  44. package/dist/src/telemetry/recorder.js +98 -0
  45. package/dist/src/telemetry/recorder.js.map +1 -0
  46. package/dist/src/telemetry/types.d.ts +74 -0
  47. package/dist/src/telemetry/types.d.ts.map +1 -0
  48. package/dist/src/telemetry/types.js +2 -0
  49. package/dist/src/telemetry/types.js.map +1 -0
  50. package/dist/src/tools/commands.d.ts +2 -1
  51. package/dist/src/tools/commands.d.ts.map +1 -1
  52. package/dist/src/tools/commands.js +10 -1
  53. package/dist/src/tools/commands.js.map +1 -1
  54. package/dist/src/types.d.ts +7 -0
  55. package/dist/src/types.d.ts.map +1 -1
  56. 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
- message = (await request.json());
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
- if (!hasId)
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
- if (message.params !== undefined && !isRecord(message.params))
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
- if (requestedProtocolVersion !== LEGACY_PROTOCOL_VERSION)
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
- if (!isRecord(params.capabilities) || !isRecord(params.clientInfo))
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
- if (!legacyRequest && version !== PROTOCOL_VERSION)
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
- if (!legacyRequest && !isRecord(meta[CLIENT_CAPABILITIES_META]))
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
- return json({
471
- jsonrpc: "2.0",
472
- id: message.id,
473
- result: await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, notify, request.signal, codeGraph),
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
- if (params.uri === CAPABILITIES_URI)
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
- return capability.status === "available" ||
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.map((item) => item.name),
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: { npmScripts: [...npmAllowed], commands: [...allowedCommands] },
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
  }