@neurynae/toolcairn-mcp 0.10.12 → 0.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -336,8 +336,8 @@ var require_logger = __commonJS({
336
336
  return mod && mod.__esModule ? mod : { "default": mod };
337
337
  };
338
338
  Object.defineProperty(exports, "__esModule", { value: true });
339
- exports.createMcpLogger = createMcpLogger27;
340
- exports.createLogger = createMcpLogger27;
339
+ exports.createMcpLogger = createMcpLogger28;
340
+ exports.createLogger = createMcpLogger28;
341
341
  var node_os_1 = __require("os");
342
342
  var node_path_1 = __require("path");
343
343
  var pino_1 = __importDefault(__require("pino"));
@@ -361,7 +361,7 @@ var require_logger = __commonJS({
361
361
  "*.apiKey",
362
362
  "*.api_key"
363
363
  ];
364
- function createMcpLogger27(opts) {
364
+ function createMcpLogger28(opts) {
365
365
  const level = opts.level ?? process.env.LOG_LEVEL ?? (process.env.NODE_ENV !== "production" ? "debug" : "info");
366
366
  const pinoOpts = {
367
367
  name: opts.name,
@@ -407,14 +407,14 @@ var require_mcp_error_wrapper = __commonJS({
407
407
  exports.withErrorHandling = withErrorHandling2;
408
408
  var error_codes_js_1 = require_error_codes();
409
409
  var errors_js_1 = require_errors();
410
- function withErrorHandling2(toolName, logger27, handler) {
410
+ function withErrorHandling2(toolName, logger28, handler) {
411
411
  return async (args) => {
412
412
  try {
413
413
  return await handler(args);
414
414
  } catch (err) {
415
415
  if (err instanceof errors_js_1.AppError) {
416
416
  const logLevel = err.severity === "critical" || err.severity === "high" ? "error" : "warn";
417
- logger27[logLevel]({ err, tool: toolName }, `Tool ${toolName} failed: ${err.message}`);
417
+ logger28[logLevel]({ err, tool: toolName }, `Tool ${toolName} failed: ${err.message}`);
418
418
  return {
419
419
  content: [
420
420
  {
@@ -429,7 +429,7 @@ var require_mcp_error_wrapper = __commonJS({
429
429
  isError: true
430
430
  };
431
431
  }
432
- logger27.error({ err, tool: toolName }, `Unexpected error in tool ${toolName}`);
432
+ logger28.error({ err, tool: toolName }, `Unexpected error in tool ${toolName}`);
433
433
  return {
434
434
  content: [
435
435
  {
@@ -512,7 +512,7 @@ var require_dist2 = __commonJS({
512
512
  // src/index.prod.ts
513
513
  init_esm_shims();
514
514
  var import_config5 = __toESM(require_dist(), 1);
515
- var import_errors28 = __toESM(require_dist2(), 1);
515
+ var import_errors29 = __toESM(require_dist2(), 1);
516
516
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
517
517
 
518
518
  // ../../packages/remote/dist/index.js
@@ -939,7 +939,7 @@ async function pollForToken(apiUrl, deviceCode, intervalSec) {
939
939
  }
940
940
  }
941
941
  function sleep(ms) {
942
- return new Promise((resolve4) => setTimeout(resolve4, ms));
942
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
943
943
  }
944
944
 
945
945
  // src/index.prod.ts
@@ -1316,10 +1316,33 @@ async function writeConfig(projectRoot, config5) {
1316
1316
  init_esm_shims();
1317
1317
  var import_errors6 = __toESM(require_dist2(), 1);
1318
1318
  import { appendFile, mkdir as mkdir3, readFile as readFile3, rm, writeFile as writeFile2 } from "fs/promises";
1319
+ import lockfile from "proper-lockfile";
1319
1320
  import writeFileAtomic2 from "write-file-atomic";
1321
+
1322
+ // ../../packages/tools-local/dist/config-store/skeleton.js
1323
+ init_esm_shims();
1324
+ function emptySkeleton(name = "") {
1325
+ return {
1326
+ version: "1.2",
1327
+ project: {
1328
+ name,
1329
+ languages: [],
1330
+ frameworks: [],
1331
+ subprojects: []
1332
+ },
1333
+ tools: {
1334
+ confirmed: [],
1335
+ pending_evaluation: [],
1336
+ unknown_in_graph: []
1337
+ },
1338
+ last_audit_entry: null
1339
+ };
1340
+ }
1341
+
1342
+ // ../../packages/tools-local/dist/config-store/audit.js
1320
1343
  var logger4 = (0, import_errors6.createMcpLogger)({ name: "@toolcairn/tools:audit-log" });
1321
- var MAX_LIVE_ENTRIES = 1e3;
1322
- var ARCHIVE_BATCH = 500;
1344
+ var MAX_LIVE_ENTRIES = 5e3;
1345
+ var ARCHIVE_BATCH = 2500;
1323
1346
  async function appendAudit(projectRoot, entry) {
1324
1347
  await mkdir3(joinConfigDir(projectRoot), { recursive: true });
1325
1348
  const auditPath = joinAuditPath(projectRoot);
@@ -1338,6 +1361,49 @@ async function bulkAppendAudit(projectRoot, entries) {
1338
1361
  await appendFile(auditPath, payload, "utf-8");
1339
1362
  await rotateIfNeeded(projectRoot, auditPath);
1340
1363
  }
1364
+ async function appendToolCallAudit(projectRoot, entry) {
1365
+ try {
1366
+ await ensureLockable(projectRoot);
1367
+ } catch (err) {
1368
+ logger4.debug({ err, projectRoot }, "audit-log: bootstrap skipped (read-only?) \u2014 abandoning");
1369
+ return;
1370
+ }
1371
+ const configPath = joinConfigPath(projectRoot);
1372
+ let release = null;
1373
+ try {
1374
+ release = await lockfile.lock(configPath, {
1375
+ stale: 1e4,
1376
+ retries: { retries: 5, minTimeout: 50, factor: 2, maxTimeout: 500 },
1377
+ realpath: false
1378
+ });
1379
+ await appendAudit(projectRoot, entry);
1380
+ } catch (err) {
1381
+ logger4.warn({ err, projectRoot }, "audit-log: tool-call append failed");
1382
+ } finally {
1383
+ if (release) {
1384
+ try {
1385
+ await release();
1386
+ } catch (err) {
1387
+ logger4.debug({ err }, "audit-log: lock release failed (likely already stale)");
1388
+ }
1389
+ }
1390
+ }
1391
+ }
1392
+ async function ensureLockable(projectRoot) {
1393
+ await mkdir3(joinConfigDir(projectRoot), { recursive: true });
1394
+ const configPath = joinConfigPath(projectRoot);
1395
+ if (!await fileExists(configPath)) {
1396
+ await writeFile2(configPath, `${JSON.stringify(emptySkeleton(), null, 2)}
1397
+ `, "utf-8");
1398
+ }
1399
+ }
1400
+ async function readLiveAudit(projectRoot) {
1401
+ const auditPath = joinAuditPath(projectRoot);
1402
+ if (!await fileExists(auditPath))
1403
+ return [];
1404
+ const raw = await readFile3(auditPath, "utf-8");
1405
+ return parseJsonl(raw);
1406
+ }
1341
1407
  async function rotateIfNeeded(projectRoot, auditPath) {
1342
1408
  const raw = await readFile3(auditPath, "utf-8");
1343
1409
  const lines = raw.split("\n").filter((l) => l.trim().length > 0);
@@ -1357,6 +1423,18 @@ async function rotateIfNeeded(projectRoot, auditPath) {
1357
1423
  logger4.warn({ err, auditPath, archivePath }, "Audit-log rotation failed \u2014 live file intact");
1358
1424
  }
1359
1425
  }
1426
+ function parseJsonl(raw) {
1427
+ const out = [];
1428
+ for (const line of raw.split("\n")) {
1429
+ if (!line.trim())
1430
+ continue;
1431
+ try {
1432
+ out.push(JSON.parse(line));
1433
+ } catch {
1434
+ }
1435
+ }
1436
+ return out;
1437
+ }
1360
1438
 
1361
1439
  // ../../packages/tools-local/dist/config-store/migrate.js
1362
1440
  init_esm_shims();
@@ -1429,35 +1507,13 @@ async function migrateToV1_2(config5, projectRoot) {
1429
1507
  init_esm_shims();
1430
1508
  var import_errors7 = __toESM(require_dist2(), 1);
1431
1509
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
1432
- import lockfile from "proper-lockfile";
1433
-
1434
- // ../../packages/tools-local/dist/config-store/skeleton.js
1435
- init_esm_shims();
1436
- function emptySkeleton(name = "") {
1437
- return {
1438
- version: "1.2",
1439
- project: {
1440
- name,
1441
- languages: [],
1442
- frameworks: [],
1443
- subprojects: []
1444
- },
1445
- tools: {
1446
- confirmed: [],
1447
- pending_evaluation: [],
1448
- unknown_in_graph: []
1449
- },
1450
- last_audit_entry: null
1451
- };
1452
- }
1453
-
1454
- // ../../packages/tools-local/dist/config-store/mutate.js
1510
+ import lockfile2 from "proper-lockfile";
1455
1511
  var logger5 = (0, import_errors7.createMcpLogger)({ name: "@toolcairn/tools:config-store" });
1456
1512
  async function mutateConfig(projectRoot, mutator, audit) {
1457
1513
  const configPath = joinConfigPath(projectRoot);
1458
1514
  const preExisted = await fileExists(configPath);
1459
1515
  await ensureLockableDir(projectRoot);
1460
- const release = await lockfile.lock(configPath, {
1516
+ const release = await lockfile2.lock(configPath, {
1461
1517
  stale: 1e4,
1462
1518
  retries: { retries: 5, minTimeout: 50, factor: 2, maxTimeout: 500 },
1463
1519
  realpath: false
@@ -4797,6 +4853,39 @@ init_esm_shims();
4797
4853
  var import_errors22 = __toESM(require_dist2(), 1);
4798
4854
  var logger20 = (0, import_errors22.createMcpLogger)({ name: "@toolcairn/tools:read-project-config" });
4799
4855
  var STALENESS_THRESHOLD_DAYS = 90;
4856
+ var PENDING_OUTCOME_TTL_DAYS = 7;
4857
+ var RECOMMENDATION_MCP_TOOLS = /* @__PURE__ */ new Set([
4858
+ "search_tools",
4859
+ "search_tools_respond",
4860
+ "get_stack"
4861
+ ]);
4862
+ function derivePendingOutcomes(entries) {
4863
+ const cutoff = Date.now() - PENDING_OUTCOME_TTL_DAYS * 24 * 60 * 60 * 1e3;
4864
+ const open = /* @__PURE__ */ new Map();
4865
+ for (const e of entries) {
4866
+ if (e.action !== "tool_call" || !e.query_id)
4867
+ continue;
4868
+ if (e.status === "error")
4869
+ continue;
4870
+ const ts = Date.parse(e.timestamp);
4871
+ if (Number.isNaN(ts) || ts < cutoff)
4872
+ continue;
4873
+ if (e.mcp_tool && RECOMMENDATION_MCP_TOOLS.has(e.mcp_tool)) {
4874
+ const queryFromMeta = typeof e.metadata?.query === "string" ? e.metadata.query : typeof e.metadata?.use_case === "string" ? e.metadata.use_case : null;
4875
+ open.set(e.query_id, {
4876
+ query_id: e.query_id,
4877
+ mcp_tool: e.mcp_tool,
4878
+ selected_at: e.timestamp,
4879
+ age_hours: Math.round((Date.now() - ts) / (1e3 * 60 * 60)),
4880
+ candidates: e.candidates ?? [],
4881
+ query: queryFromMeta
4882
+ });
4883
+ } else if (e.mcp_tool === "report_outcome") {
4884
+ open.delete(e.query_id);
4885
+ }
4886
+ }
4887
+ return Array.from(open.values()).sort((a, b) => Date.parse(a.selected_at) - Date.parse(b.selected_at));
4888
+ }
4800
4889
  function daysSince(isoDate) {
4801
4890
  return (Date.now() - new Date(isoDate).getTime()) / (1e3 * 60 * 60 * 24);
4802
4891
  }
@@ -4843,6 +4932,13 @@ async function handleReadProjectConfig(args) {
4843
4932
  });
4844
4933
  const non_oss_tools = config5.tools.confirmed.filter((t) => t.source === "non_oss").map((t) => t.name);
4845
4934
  const unknown_tools = (config5.tools.unknown_in_graph ?? []).filter((t) => !t.suggested);
4935
+ let pending_outcomes = [];
4936
+ try {
4937
+ const auditEntries = await readLiveAudit(args.project_root);
4938
+ pending_outcomes = derivePendingOutcomes(auditEntries);
4939
+ } catch (err) {
4940
+ logger20.debug({ err }, "pending_outcomes derivation skipped (audit-log unreadable)");
4941
+ }
4846
4942
  const toolcairn_indexed_tools = config5.tools.confirmed.filter((t) => t.source === "toolcairn" || t.source === "toolpilot").map((t) => t.name);
4847
4943
  const include_locations = args.include_locations === true;
4848
4944
  const confirmed_tools_detail = include_locations ? config5.tools.confirmed.map((t) => ({
@@ -4862,7 +4958,8 @@ async function handleReadProjectConfig(args) {
4862
4958
  "When recommending tools, skip any already in confirmed_tools.",
4863
4959
  non_oss_tools.length > 0 ? `Non-OSS tools in project (handle separately): ${non_oss_tools.join(", ")}` : "",
4864
4960
  staleTools.length > 0 ? `Tools that may be stale \u2014 worth re-checking: ${staleTools.map((t) => t.name).join(", ")}` : "",
4865
- unknown_tools.length > 0 ? `Unknown-to-graph tools awaiting submission (${unknown_tools.length}): ${unknown_tools.map((t) => t.name).join(", ")}. BEFORE answering the user, call suggest_graph_update (suggestion_type="new_tool", data.tools=<this list>) to STAGE them for admin review, then update_project_config action="mark_suggestions_sent" with the successful tool_names.` : ""
4961
+ unknown_tools.length > 0 ? `Unknown-to-graph tools awaiting submission (${unknown_tools.length}): ${unknown_tools.map((t) => t.name).join(", ")}. BEFORE answering the user, call suggest_graph_update (suggestion_type="new_tool", data.tools=<this list>) to STAGE them for admin review, then update_project_config action="mark_suggestions_sent" with the successful tool_names.` : "",
4962
+ pending_outcomes.length > 0 ? `Pending outcomes (${pending_outcomes.length}) \u2014 recommendations the user used but you have not yet reported on. As soon as the user uses (or replaces) one of the suggested tools, call report_outcome with the matching query_id: ${pending_outcomes.map((p) => `${p.query_id} (${p.mcp_tool}, ${p.age_hours}h ago${p.candidates.length > 0 ? `, candidates: ${p.candidates.slice(0, 3).join("/")}` : ""})`).join("; ")}.` : ""
4866
4963
  ].filter(Boolean);
4867
4964
  return okResult({
4868
4965
  status: "ready",
@@ -4883,6 +4980,8 @@ async function handleReadProjectConfig(args) {
4883
4980
  total_confirmed: confirmedToolNames.length,
4884
4981
  total_pending: pendingToolNames.length,
4885
4982
  total_unknown_undrained: unknown_tools.length,
4983
+ pending_outcomes,
4984
+ total_pending_outcomes: pending_outcomes.length,
4886
4985
  last_audit_entry: config5.last_audit_entry ?? null,
4887
4986
  scan_metadata: config5.scan_metadata ?? null,
4888
4987
  confirmed_tools_detail,
@@ -5699,17 +5798,243 @@ async function createIfAbsent(filePath, content, label) {
5699
5798
  // src/server.prod.ts
5700
5799
  init_esm_shims();
5701
5800
  var import_config3 = __toESM(require_dist(), 1);
5702
- var import_errors27 = __toESM(require_dist2(), 1);
5801
+ var import_errors28 = __toESM(require_dist2(), 1);
5703
5802
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5704
5803
  import { z as z2 } from "zod";
5705
5804
 
5805
+ // src/middleware/audit-logger.ts
5806
+ init_esm_shims();
5807
+ var import_errors26 = __toESM(require_dist2(), 1);
5808
+ import { existsSync as existsSync2 } from "fs";
5809
+ import { dirname, isAbsolute, join as join33, resolve as resolve4 } from "path";
5810
+ var logger24 = (0, import_errors26.createMcpLogger)({ name: "@toolcairn/mcp-server:audit-logger" });
5811
+ var cwdRootCache = /* @__PURE__ */ new Map();
5812
+ var RECOMMENDATION_TOOLS = /* @__PURE__ */ new Set([
5813
+ "search_tools",
5814
+ "search_tools_respond",
5815
+ "get_stack",
5816
+ "refine_requirement"
5817
+ ]);
5818
+ var SKIP_AUDIT = /* @__PURE__ */ new Set([
5819
+ // Currently empty — but reserved if e.g. a heartbeat tool is added later.
5820
+ ]);
5821
+ function withAuditLog(toolName, handler) {
5822
+ return async (args) => {
5823
+ const start = Date.now();
5824
+ let result;
5825
+ let status = "ok";
5826
+ let thrown;
5827
+ try {
5828
+ result = await handler(args);
5829
+ if (result.isError) status = "error";
5830
+ } catch (err) {
5831
+ status = "error";
5832
+ thrown = err;
5833
+ }
5834
+ const duration_ms = Date.now() - start;
5835
+ if (result && status === "ok" && RECOMMENDATION_TOOLS.has(toolName)) {
5836
+ result = injectNextActionHint(result, toolName);
5837
+ }
5838
+ if (!SKIP_AUDIT.has(toolName)) {
5839
+ const projectRoot = resolveProjectRoot(args);
5840
+ if (projectRoot) {
5841
+ const entry = buildEntry({
5842
+ toolName,
5843
+ args,
5844
+ result,
5845
+ status,
5846
+ duration_ms
5847
+ });
5848
+ appendToolCallAudit(projectRoot, entry).catch((err) => {
5849
+ logger24.debug({ err, toolName, projectRoot }, "audit-log: append failed (non-fatal)");
5850
+ });
5851
+ }
5852
+ }
5853
+ if (thrown !== void 0) throw thrown;
5854
+ return result ?? { content: [], isError: true };
5855
+ };
5856
+ }
5857
+ function resolveProjectRoot(args) {
5858
+ const explicit = args.project_root;
5859
+ if (typeof explicit === "string" && explicit.length > 0 && isAbsolute(explicit)) {
5860
+ return explicit;
5861
+ }
5862
+ return findRootForCwd(process.cwd());
5863
+ }
5864
+ function findRootForCwd(cwd) {
5865
+ const cached = cwdRootCache.get(cwd);
5866
+ if (cached !== void 0) return cached;
5867
+ let dir = resolve4(cwd);
5868
+ while (true) {
5869
+ if (existsSync2(join33(dir, ".toolcairn", "config.json"))) {
5870
+ cwdRootCache.set(cwd, dir);
5871
+ return dir;
5872
+ }
5873
+ const parent = dirname(dir);
5874
+ if (parent === dir) {
5875
+ cwdRootCache.set(cwd, null);
5876
+ return null;
5877
+ }
5878
+ dir = parent;
5879
+ }
5880
+ }
5881
+ function buildEntry(input) {
5882
+ const { toolName, args, result, status, duration_ms } = input;
5883
+ const argQueryId = typeof args.query_id === "string" ? args.query_id : void 0;
5884
+ const data = result ? extractResultData(result) : void 0;
5885
+ const responseQueryId = typeof data?.query_id === "string" ? data.query_id : void 0;
5886
+ const query_id = argQueryId ?? responseQueryId;
5887
+ const tool = pickToolLabel(toolName, args, data);
5888
+ const outcome = pickOutcome(toolName, args);
5889
+ const replaced_by = pickReplacedBy(toolName, args);
5890
+ const candidates = pickCandidates(toolName, data);
5891
+ const metadata = buildMetadata(toolName, args, data);
5892
+ const reason = buildReason(toolName, args, status);
5893
+ const entry = {
5894
+ action: "tool_call",
5895
+ tool,
5896
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5897
+ reason,
5898
+ mcp_tool: toolName,
5899
+ duration_ms,
5900
+ status
5901
+ };
5902
+ if (query_id) entry.query_id = query_id;
5903
+ if (outcome) entry.outcome = outcome;
5904
+ if (replaced_by) entry.replaced_by = replaced_by;
5905
+ if (candidates && candidates.length > 0) entry.candidates = candidates;
5906
+ if (metadata && Object.keys(metadata).length > 0) entry.metadata = metadata;
5907
+ return entry;
5908
+ }
5909
+ function pickToolLabel(toolName, args, data) {
5910
+ if (toolName === "report_outcome" && typeof args.chosen_tool === "string") {
5911
+ return args.chosen_tool;
5912
+ }
5913
+ if ((toolName === "check_compatibility" || toolName === "compare_tools") && typeof args.tool_a === "string" && typeof args.tool_b === "string") {
5914
+ return `${args.tool_a}|${args.tool_b}`;
5915
+ }
5916
+ if (toolName === "check_issue" && typeof args.tool_name === "string") {
5917
+ return args.tool_name;
5918
+ }
5919
+ if (toolName === "verify_suggestion" && Array.isArray(data?.verified)) {
5920
+ return "__verify__";
5921
+ }
5922
+ if (toolName === "update_project_config" && typeof args.tool_name === "string") {
5923
+ return args.tool_name;
5924
+ }
5925
+ return `__call__:${toolName}`;
5926
+ }
5927
+ function pickOutcome(toolName, args) {
5928
+ if (toolName !== "report_outcome") return void 0;
5929
+ const v = args.outcome;
5930
+ if (v === "success" || v === "failure" || v === "replaced" || v === "pending") return v;
5931
+ return void 0;
5932
+ }
5933
+ function pickReplacedBy(toolName, args) {
5934
+ if (toolName !== "report_outcome") return void 0;
5935
+ return typeof args.replaced_by === "string" ? args.replaced_by : void 0;
5936
+ }
5937
+ function pickCandidates(toolName, data) {
5938
+ if (!data) return void 0;
5939
+ if (toolName === "search_tools" || toolName === "search_tools_respond") {
5940
+ const results = data.results;
5941
+ if (Array.isArray(results)) {
5942
+ const names = results.map((r) => r && typeof r === "object" ? r.name : void 0).filter((n) => typeof n === "string").slice(0, 5);
5943
+ return names.length > 0 ? names : void 0;
5944
+ }
5945
+ }
5946
+ if (toolName === "get_stack") {
5947
+ const stack = data.stack;
5948
+ if (Array.isArray(stack)) {
5949
+ const names = stack.map((r) => r && typeof r === "object" ? r.name : void 0).filter((n) => typeof n === "string").slice(0, 10);
5950
+ return names.length > 0 ? names : void 0;
5951
+ }
5952
+ }
5953
+ return void 0;
5954
+ }
5955
+ function buildMetadata(toolName, args, data) {
5956
+ const meta = {};
5957
+ if (typeof args.query === "string") meta.query = truncate(args.query, 200);
5958
+ if (typeof args.use_case === "string") meta.use_case = truncate(args.use_case, 200);
5959
+ if (typeof args.action === "string") meta.config_action = args.action;
5960
+ if (typeof args.agent === "string") meta.agent = args.agent;
5961
+ if (data) {
5962
+ if (typeof data.status === "string") meta.response_status = data.status;
5963
+ if (typeof data.stage === "number") meta.stage = data.stage;
5964
+ if (typeof data.is_two_option === "boolean") meta.is_two_option = data.is_two_option;
5965
+ if (typeof data.compatibility_signal === "string")
5966
+ meta.compatibility_signal = data.compatibility_signal;
5967
+ if (typeof data.recommendation === "string") meta.recommendation = data.recommendation;
5968
+ if (typeof data.staged === "number") meta.staged = data.staged;
5969
+ }
5970
+ return meta;
5971
+ }
5972
+ function buildReason(toolName, args, status) {
5973
+ if (status === "error") return `MCP tool ${toolName} failed`;
5974
+ if (toolName === "report_outcome" && typeof args.outcome === "string") {
5975
+ const chosen = typeof args.chosen_tool === "string" ? args.chosen_tool : "?";
5976
+ return `report_outcome: ${chosen} \u2192 ${args.outcome}`;
5977
+ }
5978
+ if (toolName === "search_tools" && typeof args.query === "string") {
5979
+ return `search_tools: ${truncate(args.query, 120)}`;
5980
+ }
5981
+ if (toolName === "get_stack" && typeof args.use_case === "string") {
5982
+ return `get_stack: ${truncate(args.use_case, 120)}`;
5983
+ }
5984
+ if (toolName === "update_project_config" && typeof args.action === "string") {
5985
+ const tn = typeof args.tool_name === "string" ? ` ${args.tool_name}` : "";
5986
+ return `update_project_config: ${args.action}${tn}`;
5987
+ }
5988
+ return `MCP tool ${toolName}`;
5989
+ }
5990
+ function truncate(s, n) {
5991
+ return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
5992
+ }
5993
+ function injectNextActionHint(result, toolName) {
5994
+ try {
5995
+ const first = result.content?.[0];
5996
+ if (!first || first.type !== "text") return result;
5997
+ const parsed = JSON.parse(first.text);
5998
+ const data = parsed.data ?? void 0;
5999
+ if (!data) return result;
6000
+ const queryId = typeof data.query_id === "string" ? data.query_id : void 0;
6001
+ if (!queryId) return result;
6002
+ if (typeof data.next_action === "string" && data.next_action.length > 0) return result;
6003
+ data.next_action = buildHint(toolName, queryId);
6004
+ parsed.data = data;
6005
+ return {
6006
+ ...result,
6007
+ content: [{ type: "text", text: JSON.stringify(parsed) }]
6008
+ };
6009
+ } catch {
6010
+ return result;
6011
+ }
6012
+ }
6013
+ function buildHint(toolName, queryId) {
6014
+ if (toolName === "refine_requirement") {
6015
+ return `Use the decomposition to call get_stack or search_tools (passing query_id="${queryId}"); after the user actually uses the chosen tool, call report_outcome({ query_id: "${queryId}", chosen_tool, outcome }).`;
6016
+ }
6017
+ return `After the user actually uses one of the suggested tools (or replaces it), call report_outcome({ query_id: "${queryId}", chosen_tool, outcome }) to close the feedback loop.`;
6018
+ }
6019
+ function extractResultData(result) {
6020
+ try {
6021
+ const first = result.content?.[0];
6022
+ if (!first || first.type !== "text") return void 0;
6023
+ const parsed = JSON.parse(first.text);
6024
+ const data = parsed.data;
6025
+ return data && typeof data === "object" ? data : void 0;
6026
+ } catch {
6027
+ return void 0;
6028
+ }
6029
+ }
6030
+
5706
6031
  // src/middleware/event-logger.ts
5707
6032
  init_esm_shims();
5708
6033
  var import_config2 = __toESM(require_dist(), 1);
5709
- var import_errors26 = __toESM(require_dist2(), 1);
6034
+ var import_errors27 = __toESM(require_dist2(), 1);
5710
6035
  import { appendFile as appendFile2, mkdir as mkdir6 } from "fs/promises";
5711
- import { dirname } from "path";
5712
- var logger24 = (0, import_errors26.createMcpLogger)({ name: "@toolcairn/mcp-server:event-logger" });
6036
+ import { dirname as dirname2 } from "path";
6037
+ var logger25 = (0, import_errors27.createMcpLogger)({ name: "@toolcairn/mcp-server:event-logger" });
5713
6038
  function isTrackingEnabled() {
5714
6039
  return process.env.TOOLCAIRN_TRACKING_ENABLED !== "false";
5715
6040
  }
@@ -5749,11 +6074,11 @@ function extractMetadata(toolName, result) {
5749
6074
  }
5750
6075
  async function writeToFile(eventsPath, event) {
5751
6076
  try {
5752
- await mkdir6(dirname(eventsPath), { recursive: true });
6077
+ await mkdir6(dirname2(eventsPath), { recursive: true });
5753
6078
  await appendFile2(eventsPath, `${JSON.stringify(event)}
5754
6079
  `, "utf-8");
5755
6080
  } catch (e) {
5756
- logger24.warn({ err: e, path: eventsPath }, "Failed to write event to JSONL file");
6081
+ logger25.warn({ err: e, path: eventsPath }, "Failed to write event to JSONL file");
5757
6082
  }
5758
6083
  }
5759
6084
  async function sendToApi(event) {
@@ -5775,7 +6100,7 @@ async function sendToApi(event) {
5775
6100
  })
5776
6101
  });
5777
6102
  } catch (e) {
5778
- logger24.debug({ err: e }, "Failed to send event to API \u2014 non-fatal");
6103
+ logger25.debug({ err: e }, "Failed to send event to API \u2014 non-fatal");
5779
6104
  }
5780
6105
  }
5781
6106
  function withEventLogging(toolName, handler) {
@@ -5816,7 +6141,7 @@ function withEventLogging(toolName, handler) {
5816
6141
  }
5817
6142
 
5818
6143
  // src/server.prod.ts
5819
- var logger25 = (0, import_errors27.createMcpLogger)({ name: "@toolcairn/mcp-server:prod" });
6144
+ var logger26 = (0, import_errors28.createMcpLogger)({ name: "@toolcairn/mcp-server:prod" });
5820
6145
  var SETUP_INSTRUCTIONS = `
5821
6146
  # ToolCairn \u2014 Agent Setup Instructions
5822
6147
 
@@ -5900,9 +6225,12 @@ async function addToolsToServer(server) {
5900
6225
  apiKey: creds.client_id,
5901
6226
  accessToken: creds.access_token
5902
6227
  });
5903
- logger25.info({ user: creds.user_email }, "Registering production tools");
6228
+ logger26.info({ user: creds.user_email }, "Registering production tools");
5904
6229
  function wrap(toolName, fn) {
5905
- return withEventLogging(toolName, (0, import_errors27.withErrorHandling)(toolName, logger25, fn));
6230
+ return withEventLogging(
6231
+ toolName,
6232
+ withAuditLog(toolName, (0, import_errors28.withErrorHandling)(toolName, logger26, fn))
6233
+ );
5906
6234
  }
5907
6235
  server.registerTool(
5908
6236
  "classify_prompt",
@@ -6040,7 +6368,8 @@ async function addToolsToServer(server) {
6040
6368
  )
6041
6369
  })
6042
6370
  },
6043
- async ({ action }) => {
6371
+ wrap("toolcairn_auth", async (rawArgs) => {
6372
+ const action = rawArgs.action;
6044
6373
  if (action === "status") {
6045
6374
  const c = await loadCredentials();
6046
6375
  const isAuth = c !== null && isTokenValid(c);
@@ -6075,7 +6404,7 @@ async function addToolsToServer(server) {
6075
6404
  try {
6076
6405
  const user = await startDeviceAuth(import_config3.config.TOOLPILOT_API_URL);
6077
6406
  const initSummary = await runPostAuthInit({ agent: "claude" }).catch((err) => {
6078
- logger25.warn({ err }, "runPostAuthInit failed post-login \u2014 auth still succeeds");
6407
+ logger26.warn({ err }, "runPostAuthInit failed post-login \u2014 auth still succeeds");
6079
6408
  return null;
6080
6409
  });
6081
6410
  return {
@@ -6102,7 +6431,7 @@ async function addToolsToServer(server) {
6102
6431
  isError: true
6103
6432
  };
6104
6433
  }
6105
- }
6434
+ })
6106
6435
  );
6107
6436
  }
6108
6437
  async function buildProdServer() {
@@ -6130,14 +6459,14 @@ function createTransport() {
6130
6459
 
6131
6460
  // src/index.prod.ts
6132
6461
  process.env.TOOLPILOT_MODE = "production";
6133
- var logger26 = (0, import_errors28.createMcpLogger)({ name: "@toolcairn/mcp-server" });
6462
+ var logger27 = (0, import_errors29.createMcpLogger)({ name: "@toolcairn/mcp-server" });
6134
6463
  async function main() {
6135
6464
  await ensureProjectSetup();
6136
6465
  const creds = await loadCredentials();
6137
6466
  const authenticated = creds !== null && isTokenValid(creds);
6138
6467
  let server;
6139
6468
  if (authenticated) {
6140
- logger26.info({ user: creds.user_email }, "Authenticated \u2014 starting full server");
6469
+ logger27.info({ user: creds.user_email }, "Authenticated \u2014 starting full server");
6141
6470
  server = await buildProdServer();
6142
6471
  } else {
6143
6472
  let verificationUri = "https://toolcairn.neurynae.com/signup";
@@ -6147,15 +6476,15 @@ async function main() {
6147
6476
  if (pending) {
6148
6477
  verificationUri = pending.verification_uri;
6149
6478
  userCode = pending.user_code;
6150
- logger26.info({ userCode }, "Resuming pending sign-in");
6479
+ logger27.info({ userCode }, "Resuming pending sign-in");
6151
6480
  } else {
6152
6481
  const codeData = await requestDeviceCode(import_config5.config.TOOLPILOT_API_URL);
6153
6482
  verificationUri = codeData.verification_uri;
6154
6483
  userCode = codeData.user_code;
6155
- logger26.info({ userCode }, "New sign-in started");
6484
+ logger27.info({ userCode }, "New sign-in started");
6156
6485
  }
6157
6486
  } catch (err) {
6158
- logger26.error({ err }, "Could not reach ToolCairn API \u2014 check your connection");
6487
+ logger27.error({ err }, "Could not reach ToolCairn API \u2014 check your connection");
6159
6488
  }
6160
6489
  const instructions = userCode ? `# ToolCairn \u2014 Sign In Required
6161
6490
 
@@ -6189,17 +6518,17 @@ After sign-in the server will automatically provision .toolcairn/config.json for
6189
6518
  })
6190
6519
  );
6191
6520
  startDeviceAuth(import_config5.config.TOOLPILOT_API_URL).then(async () => {
6192
- logger26.info("Sign-in complete \u2014 adding all tools to running server");
6521
+ logger27.info("Sign-in complete \u2014 adding all tools to running server");
6193
6522
  try {
6194
6523
  await addToolsToServer(server);
6195
- logger26.info("All ToolCairn tools now available");
6524
+ logger27.info("All ToolCairn tools now available");
6196
6525
  } catch (err) {
6197
- logger26.error({ err }, "Failed to add tools after sign-in \u2014 please reconnect");
6526
+ logger27.error({ err }, "Failed to add tools after sign-in \u2014 please reconnect");
6198
6527
  return;
6199
6528
  }
6200
6529
  try {
6201
6530
  const summary = await runPostAuthInit({ agent: "claude" });
6202
- logger26.info(
6531
+ logger27.info(
6203
6532
  {
6204
6533
  roots: summary.roots_discovered.length,
6205
6534
  provisioned: summary.projects.length,
@@ -6208,18 +6537,18 @@ After sign-in the server will automatically provision .toolcairn/config.json for
6208
6537
  "Post-sign-in auto-init complete"
6209
6538
  );
6210
6539
  } catch (err) {
6211
- logger26.warn({ err }, "Post-sign-in auto-init failed \u2014 call toolcairn_init manually");
6540
+ logger27.warn({ err }, "Post-sign-in auto-init failed \u2014 call toolcairn_init manually");
6212
6541
  }
6213
6542
  }).catch((err) => {
6214
- logger26.error({ err }, "Sign-in failed \u2014 please try again");
6543
+ logger27.error({ err }, "Sign-in failed \u2014 please try again");
6215
6544
  });
6216
6545
  }
6217
6546
  const transport = createTransport();
6218
6547
  await server.connect(transport);
6219
- logger26.info(authenticated ? "ToolCairn MCP ready" : "ToolCairn MCP ready (awaiting sign-in)");
6548
+ logger27.info(authenticated ? "ToolCairn MCP ready" : "ToolCairn MCP ready (awaiting sign-in)");
6220
6549
  if (authenticated) {
6221
6550
  void runPostAuthInit({ agent: "claude" }).then((summary) => {
6222
- logger26.info(
6551
+ logger27.info(
6223
6552
  {
6224
6553
  roots: summary.roots_discovered.length,
6225
6554
  provisioned: summary.projects.length,
@@ -6228,12 +6557,12 @@ After sign-in the server will automatically provision .toolcairn/config.json for
6228
6557
  "Background auto-refresh complete"
6229
6558
  );
6230
6559
  }).catch((err) => {
6231
- logger26.warn({ err }, "Background auto-refresh failed \u2014 config left as-is");
6560
+ logger27.warn({ err }, "Background auto-refresh failed \u2014 config left as-is");
6232
6561
  });
6233
6562
  }
6234
6563
  }
6235
6564
  main().catch((error) => {
6236
- (0, import_errors28.createMcpLogger)({ name: "@toolcairn/mcp-server" }).error(
6565
+ (0, import_errors29.createMcpLogger)({ name: "@toolcairn/mcp-server" }).error(
6237
6566
  { err: error },
6238
6567
  "Failed to start MCP server"
6239
6568
  );