@neurynae/toolcairn-mcp 0.10.11 → 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 +408 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
340
|
-
exports.createLogger =
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -675,6 +675,22 @@ var ToolCairnClient = class {
|
|
|
675
675
|
async post(path2, body) {
|
|
676
676
|
try {
|
|
677
677
|
const res = await this.rawPost(path2, body);
|
|
678
|
+
if (res.status === 429) {
|
|
679
|
+
const exhausted = res.headers.get("x-toolcairn-limit-exhausted") === "1";
|
|
680
|
+
if (exhausted) {
|
|
681
|
+
try {
|
|
682
|
+
const body429 = await res.clone().json();
|
|
683
|
+
const text = body429.message ?? "ToolCairn daily limit reached. Join the Pro waitlist for 1 month free: https://toolcairn.neurynae.com/waitlist?source=limit_exhausted";
|
|
684
|
+
return {
|
|
685
|
+
content: [{ type: "text", text }]
|
|
686
|
+
// isError=false so the agent treats this as a successful tool
|
|
687
|
+
// response and relays the message to the user instead of
|
|
688
|
+
// retrying or bubbling a generic error.
|
|
689
|
+
};
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
678
694
|
const data = await res.json();
|
|
679
695
|
if (data && typeof data === "object" && "content" in data) {
|
|
680
696
|
return data;
|
|
@@ -923,7 +939,7 @@ async function pollForToken(apiUrl, deviceCode, intervalSec) {
|
|
|
923
939
|
}
|
|
924
940
|
}
|
|
925
941
|
function sleep(ms) {
|
|
926
|
-
return new Promise((
|
|
942
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
927
943
|
}
|
|
928
944
|
|
|
929
945
|
// src/index.prod.ts
|
|
@@ -1300,10 +1316,33 @@ async function writeConfig(projectRoot, config5) {
|
|
|
1300
1316
|
init_esm_shims();
|
|
1301
1317
|
var import_errors6 = __toESM(require_dist2(), 1);
|
|
1302
1318
|
import { appendFile, mkdir as mkdir3, readFile as readFile3, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1319
|
+
import lockfile from "proper-lockfile";
|
|
1303
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
|
|
1304
1343
|
var logger4 = (0, import_errors6.createMcpLogger)({ name: "@toolcairn/tools:audit-log" });
|
|
1305
|
-
var MAX_LIVE_ENTRIES =
|
|
1306
|
-
var ARCHIVE_BATCH =
|
|
1344
|
+
var MAX_LIVE_ENTRIES = 5e3;
|
|
1345
|
+
var ARCHIVE_BATCH = 2500;
|
|
1307
1346
|
async function appendAudit(projectRoot, entry) {
|
|
1308
1347
|
await mkdir3(joinConfigDir(projectRoot), { recursive: true });
|
|
1309
1348
|
const auditPath = joinAuditPath(projectRoot);
|
|
@@ -1322,6 +1361,49 @@ async function bulkAppendAudit(projectRoot, entries) {
|
|
|
1322
1361
|
await appendFile(auditPath, payload, "utf-8");
|
|
1323
1362
|
await rotateIfNeeded(projectRoot, auditPath);
|
|
1324
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
|
+
}
|
|
1325
1407
|
async function rotateIfNeeded(projectRoot, auditPath) {
|
|
1326
1408
|
const raw = await readFile3(auditPath, "utf-8");
|
|
1327
1409
|
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
@@ -1341,6 +1423,18 @@ async function rotateIfNeeded(projectRoot, auditPath) {
|
|
|
1341
1423
|
logger4.warn({ err, auditPath, archivePath }, "Audit-log rotation failed \u2014 live file intact");
|
|
1342
1424
|
}
|
|
1343
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
|
+
}
|
|
1344
1438
|
|
|
1345
1439
|
// ../../packages/tools-local/dist/config-store/migrate.js
|
|
1346
1440
|
init_esm_shims();
|
|
@@ -1413,35 +1507,13 @@ async function migrateToV1_2(config5, projectRoot) {
|
|
|
1413
1507
|
init_esm_shims();
|
|
1414
1508
|
var import_errors7 = __toESM(require_dist2(), 1);
|
|
1415
1509
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
1416
|
-
import
|
|
1417
|
-
|
|
1418
|
-
// ../../packages/tools-local/dist/config-store/skeleton.js
|
|
1419
|
-
init_esm_shims();
|
|
1420
|
-
function emptySkeleton(name = "") {
|
|
1421
|
-
return {
|
|
1422
|
-
version: "1.2",
|
|
1423
|
-
project: {
|
|
1424
|
-
name,
|
|
1425
|
-
languages: [],
|
|
1426
|
-
frameworks: [],
|
|
1427
|
-
subprojects: []
|
|
1428
|
-
},
|
|
1429
|
-
tools: {
|
|
1430
|
-
confirmed: [],
|
|
1431
|
-
pending_evaluation: [],
|
|
1432
|
-
unknown_in_graph: []
|
|
1433
|
-
},
|
|
1434
|
-
last_audit_entry: null
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// ../../packages/tools-local/dist/config-store/mutate.js
|
|
1510
|
+
import lockfile2 from "proper-lockfile";
|
|
1439
1511
|
var logger5 = (0, import_errors7.createMcpLogger)({ name: "@toolcairn/tools:config-store" });
|
|
1440
1512
|
async function mutateConfig(projectRoot, mutator, audit) {
|
|
1441
1513
|
const configPath = joinConfigPath(projectRoot);
|
|
1442
1514
|
const preExisted = await fileExists(configPath);
|
|
1443
1515
|
await ensureLockableDir(projectRoot);
|
|
1444
|
-
const release = await
|
|
1516
|
+
const release = await lockfile2.lock(configPath, {
|
|
1445
1517
|
stale: 1e4,
|
|
1446
1518
|
retries: { retries: 5, minTimeout: 50, factor: 2, maxTimeout: 500 },
|
|
1447
1519
|
realpath: false
|
|
@@ -4781,6 +4853,39 @@ init_esm_shims();
|
|
|
4781
4853
|
var import_errors22 = __toESM(require_dist2(), 1);
|
|
4782
4854
|
var logger20 = (0, import_errors22.createMcpLogger)({ name: "@toolcairn/tools:read-project-config" });
|
|
4783
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
|
+
}
|
|
4784
4889
|
function daysSince(isoDate) {
|
|
4785
4890
|
return (Date.now() - new Date(isoDate).getTime()) / (1e3 * 60 * 60 * 24);
|
|
4786
4891
|
}
|
|
@@ -4827,6 +4932,13 @@ async function handleReadProjectConfig(args) {
|
|
|
4827
4932
|
});
|
|
4828
4933
|
const non_oss_tools = config5.tools.confirmed.filter((t) => t.source === "non_oss").map((t) => t.name);
|
|
4829
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
|
+
}
|
|
4830
4942
|
const toolcairn_indexed_tools = config5.tools.confirmed.filter((t) => t.source === "toolcairn" || t.source === "toolpilot").map((t) => t.name);
|
|
4831
4943
|
const include_locations = args.include_locations === true;
|
|
4832
4944
|
const confirmed_tools_detail = include_locations ? config5.tools.confirmed.map((t) => ({
|
|
@@ -4846,7 +4958,8 @@ async function handleReadProjectConfig(args) {
|
|
|
4846
4958
|
"When recommending tools, skip any already in confirmed_tools.",
|
|
4847
4959
|
non_oss_tools.length > 0 ? `Non-OSS tools in project (handle separately): ${non_oss_tools.join(", ")}` : "",
|
|
4848
4960
|
staleTools.length > 0 ? `Tools that may be stale \u2014 worth re-checking: ${staleTools.map((t) => t.name).join(", ")}` : "",
|
|
4849
|
-
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("; ")}.` : ""
|
|
4850
4963
|
].filter(Boolean);
|
|
4851
4964
|
return okResult({
|
|
4852
4965
|
status: "ready",
|
|
@@ -4867,6 +4980,8 @@ async function handleReadProjectConfig(args) {
|
|
|
4867
4980
|
total_confirmed: confirmedToolNames.length,
|
|
4868
4981
|
total_pending: pendingToolNames.length,
|
|
4869
4982
|
total_unknown_undrained: unknown_tools.length,
|
|
4983
|
+
pending_outcomes,
|
|
4984
|
+
total_pending_outcomes: pending_outcomes.length,
|
|
4870
4985
|
last_audit_entry: config5.last_audit_entry ?? null,
|
|
4871
4986
|
scan_metadata: config5.scan_metadata ?? null,
|
|
4872
4987
|
confirmed_tools_detail,
|
|
@@ -5683,17 +5798,243 @@ async function createIfAbsent(filePath, content, label) {
|
|
|
5683
5798
|
// src/server.prod.ts
|
|
5684
5799
|
init_esm_shims();
|
|
5685
5800
|
var import_config3 = __toESM(require_dist(), 1);
|
|
5686
|
-
var
|
|
5801
|
+
var import_errors28 = __toESM(require_dist2(), 1);
|
|
5687
5802
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5688
5803
|
import { z as z2 } from "zod";
|
|
5689
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
|
+
|
|
5690
6031
|
// src/middleware/event-logger.ts
|
|
5691
6032
|
init_esm_shims();
|
|
5692
6033
|
var import_config2 = __toESM(require_dist(), 1);
|
|
5693
|
-
var
|
|
6034
|
+
var import_errors27 = __toESM(require_dist2(), 1);
|
|
5694
6035
|
import { appendFile as appendFile2, mkdir as mkdir6 } from "fs/promises";
|
|
5695
|
-
import { dirname } from "path";
|
|
5696
|
-
var
|
|
6036
|
+
import { dirname as dirname2 } from "path";
|
|
6037
|
+
var logger25 = (0, import_errors27.createMcpLogger)({ name: "@toolcairn/mcp-server:event-logger" });
|
|
5697
6038
|
function isTrackingEnabled() {
|
|
5698
6039
|
return process.env.TOOLCAIRN_TRACKING_ENABLED !== "false";
|
|
5699
6040
|
}
|
|
@@ -5733,11 +6074,11 @@ function extractMetadata(toolName, result) {
|
|
|
5733
6074
|
}
|
|
5734
6075
|
async function writeToFile(eventsPath, event) {
|
|
5735
6076
|
try {
|
|
5736
|
-
await mkdir6(
|
|
6077
|
+
await mkdir6(dirname2(eventsPath), { recursive: true });
|
|
5737
6078
|
await appendFile2(eventsPath, `${JSON.stringify(event)}
|
|
5738
6079
|
`, "utf-8");
|
|
5739
6080
|
} catch (e) {
|
|
5740
|
-
|
|
6081
|
+
logger25.warn({ err: e, path: eventsPath }, "Failed to write event to JSONL file");
|
|
5741
6082
|
}
|
|
5742
6083
|
}
|
|
5743
6084
|
async function sendToApi(event) {
|
|
@@ -5759,7 +6100,7 @@ async function sendToApi(event) {
|
|
|
5759
6100
|
})
|
|
5760
6101
|
});
|
|
5761
6102
|
} catch (e) {
|
|
5762
|
-
|
|
6103
|
+
logger25.debug({ err: e }, "Failed to send event to API \u2014 non-fatal");
|
|
5763
6104
|
}
|
|
5764
6105
|
}
|
|
5765
6106
|
function withEventLogging(toolName, handler) {
|
|
@@ -5800,7 +6141,7 @@ function withEventLogging(toolName, handler) {
|
|
|
5800
6141
|
}
|
|
5801
6142
|
|
|
5802
6143
|
// src/server.prod.ts
|
|
5803
|
-
var
|
|
6144
|
+
var logger26 = (0, import_errors28.createMcpLogger)({ name: "@toolcairn/mcp-server:prod" });
|
|
5804
6145
|
var SETUP_INSTRUCTIONS = `
|
|
5805
6146
|
# ToolCairn \u2014 Agent Setup Instructions
|
|
5806
6147
|
|
|
@@ -5884,9 +6225,12 @@ async function addToolsToServer(server) {
|
|
|
5884
6225
|
apiKey: creds.client_id,
|
|
5885
6226
|
accessToken: creds.access_token
|
|
5886
6227
|
});
|
|
5887
|
-
|
|
6228
|
+
logger26.info({ user: creds.user_email }, "Registering production tools");
|
|
5888
6229
|
function wrap(toolName, fn) {
|
|
5889
|
-
return withEventLogging(
|
|
6230
|
+
return withEventLogging(
|
|
6231
|
+
toolName,
|
|
6232
|
+
withAuditLog(toolName, (0, import_errors28.withErrorHandling)(toolName, logger26, fn))
|
|
6233
|
+
);
|
|
5890
6234
|
}
|
|
5891
6235
|
server.registerTool(
|
|
5892
6236
|
"classify_prompt",
|
|
@@ -6024,7 +6368,8 @@ async function addToolsToServer(server) {
|
|
|
6024
6368
|
)
|
|
6025
6369
|
})
|
|
6026
6370
|
},
|
|
6027
|
-
async (
|
|
6371
|
+
wrap("toolcairn_auth", async (rawArgs) => {
|
|
6372
|
+
const action = rawArgs.action;
|
|
6028
6373
|
if (action === "status") {
|
|
6029
6374
|
const c = await loadCredentials();
|
|
6030
6375
|
const isAuth = c !== null && isTokenValid(c);
|
|
@@ -6059,7 +6404,7 @@ async function addToolsToServer(server) {
|
|
|
6059
6404
|
try {
|
|
6060
6405
|
const user = await startDeviceAuth(import_config3.config.TOOLPILOT_API_URL);
|
|
6061
6406
|
const initSummary = await runPostAuthInit({ agent: "claude" }).catch((err) => {
|
|
6062
|
-
|
|
6407
|
+
logger26.warn({ err }, "runPostAuthInit failed post-login \u2014 auth still succeeds");
|
|
6063
6408
|
return null;
|
|
6064
6409
|
});
|
|
6065
6410
|
return {
|
|
@@ -6086,7 +6431,7 @@ async function addToolsToServer(server) {
|
|
|
6086
6431
|
isError: true
|
|
6087
6432
|
};
|
|
6088
6433
|
}
|
|
6089
|
-
}
|
|
6434
|
+
})
|
|
6090
6435
|
);
|
|
6091
6436
|
}
|
|
6092
6437
|
async function buildProdServer() {
|
|
@@ -6114,14 +6459,14 @@ function createTransport() {
|
|
|
6114
6459
|
|
|
6115
6460
|
// src/index.prod.ts
|
|
6116
6461
|
process.env.TOOLPILOT_MODE = "production";
|
|
6117
|
-
var
|
|
6462
|
+
var logger27 = (0, import_errors29.createMcpLogger)({ name: "@toolcairn/mcp-server" });
|
|
6118
6463
|
async function main() {
|
|
6119
6464
|
await ensureProjectSetup();
|
|
6120
6465
|
const creds = await loadCredentials();
|
|
6121
6466
|
const authenticated = creds !== null && isTokenValid(creds);
|
|
6122
6467
|
let server;
|
|
6123
6468
|
if (authenticated) {
|
|
6124
|
-
|
|
6469
|
+
logger27.info({ user: creds.user_email }, "Authenticated \u2014 starting full server");
|
|
6125
6470
|
server = await buildProdServer();
|
|
6126
6471
|
} else {
|
|
6127
6472
|
let verificationUri = "https://toolcairn.neurynae.com/signup";
|
|
@@ -6131,15 +6476,15 @@ async function main() {
|
|
|
6131
6476
|
if (pending) {
|
|
6132
6477
|
verificationUri = pending.verification_uri;
|
|
6133
6478
|
userCode = pending.user_code;
|
|
6134
|
-
|
|
6479
|
+
logger27.info({ userCode }, "Resuming pending sign-in");
|
|
6135
6480
|
} else {
|
|
6136
6481
|
const codeData = await requestDeviceCode(import_config5.config.TOOLPILOT_API_URL);
|
|
6137
6482
|
verificationUri = codeData.verification_uri;
|
|
6138
6483
|
userCode = codeData.user_code;
|
|
6139
|
-
|
|
6484
|
+
logger27.info({ userCode }, "New sign-in started");
|
|
6140
6485
|
}
|
|
6141
6486
|
} catch (err) {
|
|
6142
|
-
|
|
6487
|
+
logger27.error({ err }, "Could not reach ToolCairn API \u2014 check your connection");
|
|
6143
6488
|
}
|
|
6144
6489
|
const instructions = userCode ? `# ToolCairn \u2014 Sign In Required
|
|
6145
6490
|
|
|
@@ -6173,17 +6518,17 @@ After sign-in the server will automatically provision .toolcairn/config.json for
|
|
|
6173
6518
|
})
|
|
6174
6519
|
);
|
|
6175
6520
|
startDeviceAuth(import_config5.config.TOOLPILOT_API_URL).then(async () => {
|
|
6176
|
-
|
|
6521
|
+
logger27.info("Sign-in complete \u2014 adding all tools to running server");
|
|
6177
6522
|
try {
|
|
6178
6523
|
await addToolsToServer(server);
|
|
6179
|
-
|
|
6524
|
+
logger27.info("All ToolCairn tools now available");
|
|
6180
6525
|
} catch (err) {
|
|
6181
|
-
|
|
6526
|
+
logger27.error({ err }, "Failed to add tools after sign-in \u2014 please reconnect");
|
|
6182
6527
|
return;
|
|
6183
6528
|
}
|
|
6184
6529
|
try {
|
|
6185
6530
|
const summary = await runPostAuthInit({ agent: "claude" });
|
|
6186
|
-
|
|
6531
|
+
logger27.info(
|
|
6187
6532
|
{
|
|
6188
6533
|
roots: summary.roots_discovered.length,
|
|
6189
6534
|
provisioned: summary.projects.length,
|
|
@@ -6192,18 +6537,18 @@ After sign-in the server will automatically provision .toolcairn/config.json for
|
|
|
6192
6537
|
"Post-sign-in auto-init complete"
|
|
6193
6538
|
);
|
|
6194
6539
|
} catch (err) {
|
|
6195
|
-
|
|
6540
|
+
logger27.warn({ err }, "Post-sign-in auto-init failed \u2014 call toolcairn_init manually");
|
|
6196
6541
|
}
|
|
6197
6542
|
}).catch((err) => {
|
|
6198
|
-
|
|
6543
|
+
logger27.error({ err }, "Sign-in failed \u2014 please try again");
|
|
6199
6544
|
});
|
|
6200
6545
|
}
|
|
6201
6546
|
const transport = createTransport();
|
|
6202
6547
|
await server.connect(transport);
|
|
6203
|
-
|
|
6548
|
+
logger27.info(authenticated ? "ToolCairn MCP ready" : "ToolCairn MCP ready (awaiting sign-in)");
|
|
6204
6549
|
if (authenticated) {
|
|
6205
6550
|
void runPostAuthInit({ agent: "claude" }).then((summary) => {
|
|
6206
|
-
|
|
6551
|
+
logger27.info(
|
|
6207
6552
|
{
|
|
6208
6553
|
roots: summary.roots_discovered.length,
|
|
6209
6554
|
provisioned: summary.projects.length,
|
|
@@ -6212,12 +6557,12 @@ After sign-in the server will automatically provision .toolcairn/config.json for
|
|
|
6212
6557
|
"Background auto-refresh complete"
|
|
6213
6558
|
);
|
|
6214
6559
|
}).catch((err) => {
|
|
6215
|
-
|
|
6560
|
+
logger27.warn({ err }, "Background auto-refresh failed \u2014 config left as-is");
|
|
6216
6561
|
});
|
|
6217
6562
|
}
|
|
6218
6563
|
}
|
|
6219
6564
|
main().catch((error) => {
|
|
6220
|
-
(0,
|
|
6565
|
+
(0, import_errors29.createMcpLogger)({ name: "@toolcairn/mcp-server" }).error(
|
|
6221
6566
|
{ err: error },
|
|
6222
6567
|
"Failed to start MCP server"
|
|
6223
6568
|
);
|