@neurynae/toolcairn-mcp 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -62,13 +62,261 @@ var require_dist = __commonJS({
62
62
 
63
63
  // src/index.prod.ts
64
64
  init_esm_shims();
65
+ var import_config3 = __toESM(require_dist(), 1);
66
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
67
+
68
+ // ../../packages/remote/dist/index.js
69
+ init_esm_shims();
70
+
71
+ // ../../packages/remote/dist/client.js
72
+ init_esm_shims();
73
+ var DEFAULT_TIMEOUT_MS = 3e4;
74
+ var ToolCairnClient = class {
75
+ baseUrl;
76
+ apiKey;
77
+ timeoutMs;
78
+ accessToken;
79
+ constructor(opts) {
80
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
81
+ this.apiKey = opts.apiKey;
82
+ this.accessToken = opts.accessToken;
83
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
84
+ }
85
+ // ── Core Search ──────────────────────────────────────────────────────────
86
+ async searchTools(args) {
87
+ return this.post("/v1/search", args);
88
+ }
89
+ async searchToolsRespond(args) {
90
+ return this.post("/v1/search/respond", args);
91
+ }
92
+ // ── Graph ────────────────────────────────────────────────────────────────
93
+ async checkCompatibility(args) {
94
+ return this.post("/v1/graph/compatibility", args);
95
+ }
96
+ async compareTools(args) {
97
+ return this.post("/v1/graph/compare", args);
98
+ }
99
+ async getStack(args) {
100
+ return this.post("/v1/graph/stack", args);
101
+ }
102
+ // ── Intelligence ─────────────────────────────────────────────────────────
103
+ async refineRequirement(args) {
104
+ return this.post("/v1/intelligence/refine", args);
105
+ }
106
+ async verifySuggestion(args) {
107
+ return this.post("/v1/intelligence/verify", args);
108
+ }
109
+ async checkIssue(args) {
110
+ return this.post("/v1/intelligence/issue", args);
111
+ }
112
+ // ── Feedback ─────────────────────────────────────────────────────────────
113
+ async reportOutcome(args) {
114
+ return this.post("/v1/feedback/outcome", args);
115
+ }
116
+ async suggestGraphUpdate(args) {
117
+ return this.post("/v1/feedback/suggest", args);
118
+ }
119
+ // ── Registration ─────────────────────────────────────────────────────────
120
+ async register(clientId) {
121
+ const res = await this.rawPost("/v1/register", { client_id: clientId });
122
+ return res.json();
123
+ }
124
+ async healthCheck() {
125
+ try {
126
+ const res = await fetch(`${this.baseUrl}/v1/health`, {
127
+ signal: AbortSignal.timeout(5e3)
128
+ });
129
+ return res.ok;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ // ── Private ──────────────────────────────────────────────────────────────
135
+ async post(path, body) {
136
+ try {
137
+ const res = await this.rawPost(path, body);
138
+ const data = await res.json();
139
+ if (data && typeof data === "object" && "content" in data) {
140
+ return data;
141
+ }
142
+ return {
143
+ content: [{ type: "text", text: JSON.stringify(data) }]
144
+ };
145
+ } catch (e) {
146
+ const msg = e instanceof Error ? e.message : String(e);
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: JSON.stringify({
152
+ ok: false,
153
+ error: "network_error",
154
+ message: `ToolCairn API unreachable: ${msg}. Check your internet connection or try again later.`
155
+ })
156
+ }
157
+ ],
158
+ isError: true
159
+ };
160
+ }
161
+ }
162
+ rawPost(path, body) {
163
+ const headers = {
164
+ "Content-Type": "application/json",
165
+ "X-ToolCairn-Key": this.apiKey,
166
+ "Accept-Encoding": "gzip"
167
+ };
168
+ if (this.accessToken) {
169
+ headers.Authorization = `Bearer ${this.accessToken}`;
170
+ }
171
+ return fetch(`${this.baseUrl}${path}`, {
172
+ method: "POST",
173
+ headers,
174
+ body: JSON.stringify(body),
175
+ signal: AbortSignal.timeout(this.timeoutMs)
176
+ });
177
+ }
178
+ };
179
+
180
+ // ../../packages/remote/dist/credentials.js
181
+ init_esm_shims();
182
+ import { mkdir, readFile, writeFile } from "fs/promises";
183
+ import { homedir } from "os";
184
+ import { join } from "path";
185
+ var CREDENTIALS_DIR = join(homedir(), ".toolcairn");
186
+ var CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
187
+ function isTokenValid(creds) {
188
+ if (!creds.access_token)
189
+ return false;
190
+ try {
191
+ const parts = creds.access_token.split(".");
192
+ if (parts.length !== 3)
193
+ return false;
194
+ const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf-8"));
195
+ if (payload.exp && payload.exp < Date.now() / 1e3 + 300)
196
+ return false;
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
202
+ async function loadCredentials() {
203
+ try {
204
+ const raw = await readFile(CREDENTIALS_FILE, "utf-8");
205
+ return JSON.parse(raw);
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ async function loadOrCreateCredentials() {
211
+ const existing = await loadCredentials();
212
+ if (existing)
213
+ return existing;
214
+ const creds = {
215
+ client_id: crypto.randomUUID(),
216
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
217
+ };
218
+ await saveCredentials(creds);
219
+ return creds;
220
+ }
221
+ async function saveCredentials(creds) {
222
+ await mkdir(CREDENTIALS_DIR, { recursive: true });
223
+ await writeFile(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), "utf-8");
224
+ }
225
+ async function upgradeToAuthenticated(accessToken, apiKey, user) {
226
+ const existing = await loadOrCreateCredentials();
227
+ await saveCredentials({
228
+ ...existing,
229
+ client_id: apiKey,
230
+ access_token: accessToken,
231
+ user_id: user.id,
232
+ user_email: user.email ?? void 0,
233
+ user_name: user.name ?? void 0,
234
+ authenticated_at: (/* @__PURE__ */ new Date()).toISOString()
235
+ });
236
+ }
237
+ async function clearAuthentication() {
238
+ const existing = await loadOrCreateCredentials();
239
+ await saveCredentials({
240
+ client_id: existing.client_id,
241
+ created_at: existing.created_at,
242
+ api_url: existing.api_url
243
+ });
244
+ }
245
+
246
+ // ../../packages/remote/dist/device-auth.js
247
+ init_esm_shims();
248
+ async function openBrowser(url) {
249
+ const platform2 = process.platform;
250
+ const { execSync } = await import("child_process");
251
+ try {
252
+ if (platform2 === "win32")
253
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
254
+ else if (platform2 === "darwin")
255
+ execSync(`open "${url}"`, { stdio: "ignore" });
256
+ else
257
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
258
+ } catch {
259
+ }
260
+ }
261
+ async function startDeviceAuth(apiUrl) {
262
+ const codeRes = await fetch(`${apiUrl}/v1/auth/device-code`, { method: "POST" });
263
+ if (!codeRes.ok)
264
+ throw new Error("Failed to start device auth. Check your connection.");
265
+ const codeData = await codeRes.json();
266
+ console.error("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
267
+ console.error(" Authenticate ToolCairn MCP");
268
+ console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
269
+ console.error("\n Open this URL in your browser:\n");
270
+ console.error(` ${codeData.verification_uri}
271
+ `);
272
+ console.error(` Your device code: ${codeData.user_code}`);
273
+ console.error("\n Waiting for authorization...");
274
+ console.error(" (Press Ctrl+C to cancel)\n");
275
+ await openBrowser(codeData.verification_uri);
276
+ const result = await pollForToken(apiUrl, codeData.device_code, codeData.interval);
277
+ await upgradeToAuthenticated(result.access_token, result.api_key, result.user);
278
+ console.error(`
279
+ \u2713 Authenticated as ${result.user.email}
280
+ `);
281
+ return {
282
+ userId: result.user.id,
283
+ email: result.user.email ?? "",
284
+ name: result.user.name
285
+ };
286
+ }
287
+ async function pollForToken(apiUrl, deviceCode, intervalSec) {
288
+ const intervalMs = Math.max(intervalSec, 5) * 1e3;
289
+ while (true) {
290
+ await sleep(intervalMs);
291
+ const res = await fetch(`${apiUrl}/v1/auth/token`, {
292
+ method: "POST",
293
+ headers: { "Content-Type": "application/json" },
294
+ body: JSON.stringify({ device_code: deviceCode, grant_type: "device_code" })
295
+ });
296
+ const data = await res.json();
297
+ if (data.error === "authorization_pending")
298
+ continue;
299
+ if (data.error === "expired_token")
300
+ throw new Error("Device code expired. Please try again.");
301
+ if (data.error)
302
+ throw new Error(`Authorization failed: ${data.error}`);
303
+ if (data.access_token)
304
+ return data;
305
+ }
306
+ }
307
+ function sleep(ms) {
308
+ return new Promise((resolve) => setTimeout(resolve, ms));
309
+ }
310
+
311
+ // src/index.prod.ts
65
312
  import pino9 from "pino";
313
+ import { z as z3 } from "zod";
66
314
 
67
315
  // src/project-setup.ts
68
316
  init_esm_shims();
69
- import { access, mkdir, writeFile } from "fs/promises";
317
+ import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
70
318
  import { platform, type } from "os";
71
- import { join } from "path";
319
+ import { join as join2 } from "path";
72
320
  import pino from "pino";
73
321
 
74
322
  // src/tools/generate-tracker.ts
@@ -414,381 +662,138 @@ function renderInsights() {
414
662
  if (m.auto_graduated) {
415
663
  insights.push({ tool: 'suggest_graph_update', text: 'New edge auto-graduated to graph (confidence \u22650.8)', time: ev.created_at });
416
664
  }
417
- if (m.had_non_indexed_guidance) {
418
- insights.push({ tool: ev.tool_name, text: 'Non-indexed tool detected \u2014 non-OSS guidance provided', time: ev.created_at });
419
- }
420
- if (m.recommendation) {
421
- insights.push({ tool: 'compare_tools', text: \`Tool comparison recommended: \${m.recommendation}\`, time: ev.created_at });
422
- }
423
- }
424
- const list = document.getElementById('insightsList');
425
- if (insights.length === 0) {
426
- list.innerHTML = '<li style="color:var(--muted);font-size:12px">No insights yet</li>';
427
- return;
428
- }
429
- list.innerHTML = insights.slice(0, 8).map(i => \`
430
- <li class="insight-item">
431
- <div class="i-tool">\${i.tool}</div>
432
- <div class="i-text">\${i.text}</div>
433
- </li>
434
- \`).join('');
435
- }
436
-
437
- function selectEvent(id) {
438
- selectedId = id;
439
- document.querySelectorAll('.event-row').forEach(r => r.classList.toggle('selected', r.dataset.id === id));
440
- const ev = allEvents.find(e => e.id === id);
441
- if (!ev) return;
442
- const panel = document.getElementById('detailPanel');
443
- const content = document.getElementById('detailContent');
444
- panel.style.display = 'block';
445
- const m = ev.metadata || {};
446
- const rows = [
447
- ['Tool', ev.tool_name],
448
- ['Status', ev.status],
449
- ['Duration', ev.duration_ms + 'ms'],
450
- ['Time', new Date(ev.created_at).toLocaleString()],
451
- ev.query_id ? ['Session ID', ev.query_id.slice(0, 8) + '...'] : null,
452
- ...Object.entries(m).filter(([k]) => k !== 'tool').map(([k, v]) => [k, String(v)])
453
- ].filter(Boolean);
454
- content.innerHTML = rows.map(([k, v]) => {
455
- const cls = v === 'true' || v === 'ok' ? 'green' : v === 'false' || v === 'error' ? 'red' : '';
456
- return \`<div class="kv"><span class="k">\${k}</span><span class="v \${cls}">\${v}</span></div>\`;
457
- }).join('');
458
- }
459
-
460
- function renderAll() {
461
- renderFeed();
462
- renderMetrics();
463
- renderToolChart();
464
- renderInsights();
465
- }
466
-
467
- // \u2500\u2500\u2500 Boot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
468
- if (!EVENTS_PATH || EVENTS_PATH === 'null') {
469
- document.getElementById('statusText').textContent = 'No events path configured';
470
- document.getElementById('emptyState').querySelector('p').textContent = 'TOOLCAIRN_EVENTS_PATH not set in MCP server environment';
471
- } else {
472
- startPolling();
473
- }
474
- </script>
475
- </body>
476
- </html>`;
477
- }
478
-
479
- // src/project-setup.ts
480
- var logger = pino({ name: "@toolcairn/mcp-server:project-setup" });
481
- var INITIAL_CONFIG = {
482
- version: "1.0",
483
- project: {
484
- name: "",
485
- language: "",
486
- framework: ""
487
- },
488
- tools: {
489
- confirmed: [],
490
- pending_evaluation: []
491
- },
492
- audit_log: []
493
- };
494
- function detectOs() {
495
- const p = platform();
496
- const labels = {
497
- win32: "Windows",
498
- darwin: "macOS",
499
- linux: "Linux",
500
- freebsd: "FreeBSD",
501
- openbsd: "OpenBSD",
502
- sunos: "Solaris",
503
- android: "Android"
504
- };
505
- return { platform: p, label: labels[p] ?? type() };
506
- }
507
- function toFileUrl(absPath) {
508
- return absPath.replace(/\\/g, "/");
509
- }
510
- async function ensureProjectSetup(projectRoot = process.cwd()) {
511
- const os = detectOs();
512
- logger.info(
513
- { os: os.label, platform: os.platform, projectRoot },
514
- "Detected OS \u2014 starting project setup"
515
- );
516
- const dir = join(projectRoot, ".toolcairn");
517
- const configPath = join(dir, "config.json");
518
- const trackerPath = join(dir, "tracker.html");
519
- const eventsPath = join(dir, "events.jsonl");
520
- const eventsPathForUrl = toFileUrl(eventsPath);
521
- try {
522
- await mkdir(dir, { recursive: true });
523
- await createIfAbsent(configPath, JSON.stringify(INITIAL_CONFIG, null, 2), "config.json");
524
- await createIfAbsent(trackerPath, generateTrackerHtml(eventsPathForUrl), "tracker.html");
525
- await createIfAbsent(eventsPath, "", "events.jsonl");
526
- logger.info({ dir, os: os.label }, ".toolcairn setup ready");
527
- } catch (e) {
528
- logger.warn(
529
- { err: e, dir, os: os.label },
530
- "Project setup failed \u2014 continuing without .toolcairn files"
531
- );
532
- }
533
- }
534
- async function createIfAbsent(filePath, content, label) {
535
- try {
536
- await access(filePath);
537
- logger.debug({ file: label }, "Already exists \u2014 skipping");
538
- } catch {
539
- await writeFile(filePath, content, "utf-8");
540
- logger.info({ file: label }, "Created");
541
- }
542
- }
543
-
544
- // src/server.prod.ts
545
- init_esm_shims();
546
- var import_config = __toESM(require_dist(), 1);
547
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
548
-
549
- // ../../packages/remote/dist/index.js
550
- init_esm_shims();
551
-
552
- // ../../packages/remote/dist/client.js
553
- init_esm_shims();
554
- var DEFAULT_TIMEOUT_MS = 3e4;
555
- var ToolCairnClient = class {
556
- baseUrl;
557
- apiKey;
558
- timeoutMs;
559
- accessToken;
560
- constructor(opts) {
561
- this.baseUrl = opts.baseUrl.replace(/\/$/, "");
562
- this.apiKey = opts.apiKey;
563
- this.accessToken = opts.accessToken;
564
- this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
565
- }
566
- // ── Core Search ──────────────────────────────────────────────────────────
567
- async searchTools(args) {
568
- return this.post("/v1/search", args);
569
- }
570
- async searchToolsRespond(args) {
571
- return this.post("/v1/search/respond", args);
572
- }
573
- // ── Graph ────────────────────────────────────────────────────────────────
574
- async checkCompatibility(args) {
575
- return this.post("/v1/graph/compatibility", args);
576
- }
577
- async compareTools(args) {
578
- return this.post("/v1/graph/compare", args);
579
- }
580
- async getStack(args) {
581
- return this.post("/v1/graph/stack", args);
582
- }
583
- // ── Intelligence ─────────────────────────────────────────────────────────
584
- async refineRequirement(args) {
585
- return this.post("/v1/intelligence/refine", args);
586
- }
587
- async verifySuggestion(args) {
588
- return this.post("/v1/intelligence/verify", args);
589
- }
590
- async checkIssue(args) {
591
- return this.post("/v1/intelligence/issue", args);
592
- }
593
- // ── Feedback ─────────────────────────────────────────────────────────────
594
- async reportOutcome(args) {
595
- return this.post("/v1/feedback/outcome", args);
596
- }
597
- async suggestGraphUpdate(args) {
598
- return this.post("/v1/feedback/suggest", args);
599
- }
600
- // ── Registration ─────────────────────────────────────────────────────────
601
- async register(clientId) {
602
- const res = await this.rawPost("/v1/register", { client_id: clientId });
603
- return res.json();
604
- }
605
- async healthCheck() {
606
- try {
607
- const res = await fetch(`${this.baseUrl}/v1/health`, {
608
- signal: AbortSignal.timeout(5e3)
609
- });
610
- return res.ok;
611
- } catch {
612
- return false;
613
- }
614
- }
615
- // ── Private ──────────────────────────────────────────────────────────────
616
- async post(path, body) {
617
- try {
618
- const res = await this.rawPost(path, body);
619
- const data = await res.json();
620
- if (data && typeof data === "object" && "content" in data) {
621
- return data;
622
- }
623
- return {
624
- content: [{ type: "text", text: JSON.stringify(data) }]
625
- };
626
- } catch (e) {
627
- const msg = e instanceof Error ? e.message : String(e);
628
- return {
629
- content: [
630
- {
631
- type: "text",
632
- text: JSON.stringify({
633
- ok: false,
634
- error: "network_error",
635
- message: `ToolCairn API unreachable: ${msg}. Check your internet connection or try again later.`
636
- })
637
- }
638
- ],
639
- isError: true
640
- };
641
- }
642
- }
643
- rawPost(path, body) {
644
- const headers = {
645
- "Content-Type": "application/json",
646
- "X-ToolCairn-Key": this.apiKey,
647
- "Accept-Encoding": "gzip"
648
- };
649
- if (this.accessToken) {
650
- headers.Authorization = `Bearer ${this.accessToken}`;
651
- }
652
- return fetch(`${this.baseUrl}${path}`, {
653
- method: "POST",
654
- headers,
655
- body: JSON.stringify(body),
656
- signal: AbortSignal.timeout(this.timeoutMs)
657
- });
658
- }
659
- };
660
-
661
- // ../../packages/remote/dist/credentials.js
662
- init_esm_shims();
663
- import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
664
- import { homedir } from "os";
665
- import { join as join2 } from "path";
666
- var CREDENTIALS_DIR = join2(homedir(), ".toolcairn");
667
- var CREDENTIALS_FILE = join2(CREDENTIALS_DIR, "credentials.json");
668
- function isTokenValid(creds) {
669
- if (!creds.access_token)
670
- return false;
671
- try {
672
- const parts = creds.access_token.split(".");
673
- if (parts.length !== 3)
674
- return false;
675
- const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf-8"));
676
- if (payload.exp && payload.exp < Date.now() / 1e3 + 300)
677
- return false;
678
- return true;
679
- } catch {
680
- return false;
665
+ if (m.had_non_indexed_guidance) {
666
+ insights.push({ tool: ev.tool_name, text: 'Non-indexed tool detected \u2014 non-OSS guidance provided', time: ev.created_at });
667
+ }
668
+ if (m.recommendation) {
669
+ insights.push({ tool: 'compare_tools', text: \`Tool comparison recommended: \${m.recommendation}\`, time: ev.created_at });
670
+ }
681
671
  }
682
- }
683
- async function loadCredentials() {
684
- try {
685
- const raw = await readFile(CREDENTIALS_FILE, "utf-8");
686
- return JSON.parse(raw);
687
- } catch {
688
- return null;
672
+ const list = document.getElementById('insightsList');
673
+ if (insights.length === 0) {
674
+ list.innerHTML = '<li style="color:var(--muted);font-size:12px">No insights yet</li>';
675
+ return;
689
676
  }
677
+ list.innerHTML = insights.slice(0, 8).map(i => \`
678
+ <li class="insight-item">
679
+ <div class="i-tool">\${i.tool}</div>
680
+ <div class="i-text">\${i.text}</div>
681
+ </li>
682
+ \`).join('');
690
683
  }
691
- async function loadOrCreateCredentials() {
692
- const existing = await loadCredentials();
693
- if (existing)
694
- return existing;
695
- const creds = {
696
- client_id: crypto.randomUUID(),
697
- created_at: (/* @__PURE__ */ new Date()).toISOString()
698
- };
699
- await saveCredentials(creds);
700
- return creds;
684
+
685
+ function selectEvent(id) {
686
+ selectedId = id;
687
+ document.querySelectorAll('.event-row').forEach(r => r.classList.toggle('selected', r.dataset.id === id));
688
+ const ev = allEvents.find(e => e.id === id);
689
+ if (!ev) return;
690
+ const panel = document.getElementById('detailPanel');
691
+ const content = document.getElementById('detailContent');
692
+ panel.style.display = 'block';
693
+ const m = ev.metadata || {};
694
+ const rows = [
695
+ ['Tool', ev.tool_name],
696
+ ['Status', ev.status],
697
+ ['Duration', ev.duration_ms + 'ms'],
698
+ ['Time', new Date(ev.created_at).toLocaleString()],
699
+ ev.query_id ? ['Session ID', ev.query_id.slice(0, 8) + '...'] : null,
700
+ ...Object.entries(m).filter(([k]) => k !== 'tool').map(([k, v]) => [k, String(v)])
701
+ ].filter(Boolean);
702
+ content.innerHTML = rows.map(([k, v]) => {
703
+ const cls = v === 'true' || v === 'ok' ? 'green' : v === 'false' || v === 'error' ? 'red' : '';
704
+ return \`<div class="kv"><span class="k">\${k}</span><span class="v \${cls}">\${v}</span></div>\`;
705
+ }).join('');
701
706
  }
702
- async function saveCredentials(creds) {
703
- await mkdir2(CREDENTIALS_DIR, { recursive: true });
704
- await writeFile2(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), "utf-8");
707
+
708
+ function renderAll() {
709
+ renderFeed();
710
+ renderMetrics();
711
+ renderToolChart();
712
+ renderInsights();
705
713
  }
706
- async function upgradeToAuthenticated(accessToken, apiKey, user) {
707
- const existing = await loadOrCreateCredentials();
708
- await saveCredentials({
709
- ...existing,
710
- client_id: apiKey,
711
- access_token: accessToken,
712
- user_id: user.id,
713
- user_email: user.email ?? void 0,
714
- user_name: user.name ?? void 0,
715
- authenticated_at: (/* @__PURE__ */ new Date()).toISOString()
716
- });
714
+
715
+ // \u2500\u2500\u2500 Boot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
716
+ if (!EVENTS_PATH || EVENTS_PATH === 'null') {
717
+ document.getElementById('statusText').textContent = 'No events path configured';
718
+ document.getElementById('emptyState').querySelector('p').textContent = 'TOOLCAIRN_EVENTS_PATH not set in MCP server environment';
719
+ } else {
720
+ startPolling();
717
721
  }
718
- async function clearAuthentication() {
719
- const existing = await loadOrCreateCredentials();
720
- await saveCredentials({
721
- client_id: existing.client_id,
722
- created_at: existing.created_at,
723
- api_url: existing.api_url
724
- });
722
+ </script>
723
+ </body>
724
+ </html>`;
725
725
  }
726
726
 
727
- // ../../packages/remote/dist/device-auth.js
728
- init_esm_shims();
729
- async function openBrowser(url) {
730
- const platform2 = process.platform;
731
- const { execSync } = await import("child_process");
732
- try {
733
- if (platform2 === "win32")
734
- execSync(`start "" "${url}"`, { stdio: "ignore" });
735
- else if (platform2 === "darwin")
736
- execSync(`open "${url}"`, { stdio: "ignore" });
737
- else
738
- execSync(`xdg-open "${url}"`, { stdio: "ignore" });
739
- } catch {
740
- }
741
- }
742
- async function startDeviceAuth(apiUrl) {
743
- const codeRes = await fetch(`${apiUrl}/v1/auth/device-code`, { method: "POST" });
744
- if (!codeRes.ok)
745
- throw new Error("Failed to start device auth. Check your connection.");
746
- const codeData = await codeRes.json();
747
- console.error("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
748
- console.error(" Authenticate ToolCairn MCP");
749
- console.error("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
750
- console.error("\n Open this URL in your browser:\n");
751
- console.error(` ${codeData.verification_uri}
752
- `);
753
- console.error(` Your device code: ${codeData.user_code}`);
754
- console.error("\n Waiting for authorization...");
755
- console.error(" (Press Ctrl+C to cancel)\n");
756
- await openBrowser(codeData.verification_uri);
757
- const result = await pollForToken(apiUrl, codeData.device_code, codeData.interval);
758
- await upgradeToAuthenticated(result.access_token, result.api_key, result.user);
759
- console.error(`
760
- \u2713 Authenticated as ${result.user.email}
761
- `);
762
- return {
763
- userId: result.user.id,
764
- email: result.user.email ?? "",
765
- name: result.user.name
727
+ // src/project-setup.ts
728
+ var logger = pino({ name: "@toolcairn/mcp-server:project-setup" });
729
+ var INITIAL_CONFIG = {
730
+ version: "1.0",
731
+ project: {
732
+ name: "",
733
+ language: "",
734
+ framework: ""
735
+ },
736
+ tools: {
737
+ confirmed: [],
738
+ pending_evaluation: []
739
+ },
740
+ audit_log: []
741
+ };
742
+ function detectOs() {
743
+ const p = platform();
744
+ const labels = {
745
+ win32: "Windows",
746
+ darwin: "macOS",
747
+ linux: "Linux",
748
+ freebsd: "FreeBSD",
749
+ openbsd: "OpenBSD",
750
+ sunos: "Solaris",
751
+ android: "Android"
766
752
  };
753
+ return { platform: p, label: labels[p] ?? type() };
767
754
  }
768
- async function pollForToken(apiUrl, deviceCode, intervalSec) {
769
- const intervalMs = Math.max(intervalSec, 5) * 1e3;
770
- while (true) {
771
- await sleep(intervalMs);
772
- const res = await fetch(`${apiUrl}/v1/auth/token`, {
773
- method: "POST",
774
- headers: { "Content-Type": "application/json" },
775
- body: JSON.stringify({ device_code: deviceCode, grant_type: "device_code" })
776
- });
777
- const data = await res.json();
778
- if (data.error === "authorization_pending")
779
- continue;
780
- if (data.error === "expired_token")
781
- throw new Error("Device code expired. Please try again.");
782
- if (data.error)
783
- throw new Error(`Authorization failed: ${data.error}`);
784
- if (data.access_token)
785
- return data;
755
+ function toFileUrl(absPath) {
756
+ return absPath.replace(/\\/g, "/");
757
+ }
758
+ async function ensureProjectSetup(projectRoot = process.cwd()) {
759
+ const os = detectOs();
760
+ logger.info(
761
+ { os: os.label, platform: os.platform, projectRoot },
762
+ "Detected OS \u2014 starting project setup"
763
+ );
764
+ const dir = join2(projectRoot, ".toolcairn");
765
+ const configPath = join2(dir, "config.json");
766
+ const trackerPath = join2(dir, "tracker.html");
767
+ const eventsPath = join2(dir, "events.jsonl");
768
+ const eventsPathForUrl = toFileUrl(eventsPath);
769
+ try {
770
+ await mkdir2(dir, { recursive: true });
771
+ await createIfAbsent(configPath, JSON.stringify(INITIAL_CONFIG, null, 2), "config.json");
772
+ await createIfAbsent(trackerPath, generateTrackerHtml(eventsPathForUrl), "tracker.html");
773
+ await createIfAbsent(eventsPath, "", "events.jsonl");
774
+ logger.info({ dir, os: os.label }, ".toolcairn setup ready");
775
+ } catch (e) {
776
+ logger.warn(
777
+ { err: e, dir, os: os.label },
778
+ "Project setup failed \u2014 continuing without .toolcairn files"
779
+ );
786
780
  }
787
781
  }
788
- function sleep(ms) {
789
- return new Promise((resolve) => setTimeout(resolve, ms));
782
+ async function createIfAbsent(filePath, content, label) {
783
+ try {
784
+ await access(filePath);
785
+ logger.debug({ file: label }, "Already exists \u2014 skipping");
786
+ } catch {
787
+ await writeFile2(filePath, content, "utf-8");
788
+ logger.info({ file: label }, "Created");
789
+ }
790
790
  }
791
791
 
792
+ // src/server.prod.ts
793
+ init_esm_shims();
794
+ var import_config = __toESM(require_dist(), 1);
795
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
796
+
792
797
  // ../../packages/tools/dist/local.js
793
798
  init_esm_shims();
794
799
 
@@ -1647,7 +1652,7 @@ async function handleInitProjectConfig(args) {
1647
1652
  chosen_reason: "Auto-detected from project files during toolpilot_init",
1648
1653
  alternatives_considered: []
1649
1654
  }));
1650
- const config3 = {
1655
+ const config4 = {
1651
1656
  version: "1.0",
1652
1657
  project: {
1653
1658
  name: args.project_name,
@@ -1667,7 +1672,7 @@ async function handleInitProjectConfig(args) {
1667
1672
  }
1668
1673
  ]
1669
1674
  };
1670
- const config_json = JSON.stringify(config3, null, 2);
1675
+ const config_json = JSON.stringify(config4, null, 2);
1671
1676
  return okResult({
1672
1677
  config_json,
1673
1678
  file_path: ".toolpilot/config.json",
@@ -1692,18 +1697,18 @@ function daysSince(isoDate) {
1692
1697
  async function handleReadProjectConfig(args) {
1693
1698
  try {
1694
1699
  logger5.info("read_project_config called");
1695
- let config3;
1700
+ let config4;
1696
1701
  try {
1697
- config3 = JSON.parse(args.config_content);
1702
+ config4 = JSON.parse(args.config_content);
1698
1703
  } catch {
1699
1704
  return errResult("parse_error", "config_content is not valid JSON");
1700
1705
  }
1701
- if (config3.version !== "1.0") {
1702
- return errResult("version_error", `Unsupported config version: ${config3.version}`);
1706
+ if (config4.version !== "1.0") {
1707
+ return errResult("version_error", `Unsupported config version: ${config4.version}`);
1703
1708
  }
1704
- const confirmedToolNames = config3.tools.confirmed.map((t) => t.name);
1705
- const pendingToolNames = config3.tools.pending_evaluation.map((t) => t.name);
1706
- const staleTools = config3.tools.confirmed.filter((t) => {
1709
+ const confirmedToolNames = config4.tools.confirmed.map((t) => t.name);
1710
+ const pendingToolNames = config4.tools.pending_evaluation.map((t) => t.name);
1711
+ const staleTools = config4.tools.confirmed.filter((t) => {
1707
1712
  const date = t.last_verified ?? t.chosen_at ?? t.confirmed_at;
1708
1713
  return date ? daysSince(date) > STALENESS_THRESHOLD_DAYS : true;
1709
1714
  }).map((t) => {
@@ -1716,10 +1721,10 @@ async function handleReadProjectConfig(args) {
1716
1721
  recommendation: "Consider using check_issue to verify no new known issues"
1717
1722
  };
1718
1723
  });
1719
- const non_oss_tools = config3.tools.confirmed.filter((t) => t.source === "non_oss").map((t) => t.name);
1720
- const toolpilot_indexed_tools = config3.tools.confirmed.filter((t) => t.source === "toolpilot").map((t) => t.name);
1724
+ const non_oss_tools = config4.tools.confirmed.filter((t) => t.source === "non_oss").map((t) => t.name);
1725
+ const toolpilot_indexed_tools = config4.tools.confirmed.filter((t) => t.source === "toolpilot").map((t) => t.name);
1721
1726
  return okResult({
1722
- project: config3.project,
1727
+ project: config4.project,
1723
1728
  confirmed_tools: confirmedToolNames,
1724
1729
  pending_tools: pendingToolNames,
1725
1730
  non_oss_tools,
@@ -1727,9 +1732,9 @@ async function handleReadProjectConfig(args) {
1727
1732
  stale_tools: staleTools,
1728
1733
  total_confirmed: confirmedToolNames.length,
1729
1734
  total_pending: pendingToolNames.length,
1730
- last_audit_entry: config3.audit_log.at(-1) ?? null,
1735
+ last_audit_entry: config4.audit_log.at(-1) ?? null,
1731
1736
  agent_instructions: [
1732
- `Project: ${config3.project.name} (${config3.project.language}${config3.project.framework ? `, ${config3.project.framework}` : ""})`,
1737
+ `Project: ${config4.project.name} (${config4.project.language}${config4.project.framework ? `, ${config4.project.framework}` : ""})`,
1733
1738
  `Already confirmed tools: ${confirmedToolNames.join(", ") || "none"}`,
1734
1739
  "When recommending tools, skip any already in confirmed_tools.",
1735
1740
  non_oss_tools.length > 0 ? `Non-OSS tools in project (handle separately): ${non_oss_tools.join(", ")}` : "",
@@ -1749,9 +1754,9 @@ var logger6 = pino6({ name: "@toolpilot/tools:update-project-config" });
1749
1754
  async function handleUpdateProjectConfig(args) {
1750
1755
  try {
1751
1756
  logger6.info({ action: args.action, tool: args.tool_name }, "update_project_config called");
1752
- let config3;
1757
+ let config4;
1753
1758
  try {
1754
- config3 = JSON.parse(args.current_config);
1759
+ config4 = JSON.parse(args.current_config);
1755
1760
  } catch {
1756
1761
  return errResult("parse_error", "current_config is not valid JSON");
1757
1762
  }
@@ -1759,8 +1764,8 @@ async function handleUpdateProjectConfig(args) {
1759
1764
  const data = args.data ?? {};
1760
1765
  switch (args.action) {
1761
1766
  case "add_tool": {
1762
- config3.tools.pending_evaluation = config3.tools.pending_evaluation.filter((t) => t.name !== args.tool_name);
1763
- if (!config3.tools.confirmed.some((t) => t.name === args.tool_name)) {
1767
+ config4.tools.pending_evaluation = config4.tools.pending_evaluation.filter((t) => t.name !== args.tool_name);
1768
+ if (!config4.tools.confirmed.some((t) => t.name === args.tool_name)) {
1764
1769
  const newTool = {
1765
1770
  name: args.tool_name,
1766
1771
  source: data.source ?? "toolpilot",
@@ -1772,9 +1777,9 @@ async function handleUpdateProjectConfig(args) {
1772
1777
  query_id: data.query_id,
1773
1778
  notes: data.notes
1774
1779
  };
1775
- config3.tools.confirmed.push(newTool);
1780
+ config4.tools.confirmed.push(newTool);
1776
1781
  }
1777
- config3.audit_log.push({
1782
+ config4.audit_log.push({
1778
1783
  action: "add_tool",
1779
1784
  tool: args.tool_name,
1780
1785
  timestamp: now,
@@ -1783,9 +1788,9 @@ async function handleUpdateProjectConfig(args) {
1783
1788
  break;
1784
1789
  }
1785
1790
  case "remove_tool": {
1786
- config3.tools.confirmed = config3.tools.confirmed.filter((t) => t.name !== args.tool_name);
1787
- config3.tools.pending_evaluation = config3.tools.pending_evaluation.filter((t) => t.name !== args.tool_name);
1788
- config3.audit_log.push({
1791
+ config4.tools.confirmed = config4.tools.confirmed.filter((t) => t.name !== args.tool_name);
1792
+ config4.tools.pending_evaluation = config4.tools.pending_evaluation.filter((t) => t.name !== args.tool_name);
1793
+ config4.audit_log.push({
1789
1794
  action: "remove_tool",
1790
1795
  tool: args.tool_name,
1791
1796
  timestamp: now,
@@ -1794,22 +1799,22 @@ async function handleUpdateProjectConfig(args) {
1794
1799
  break;
1795
1800
  }
1796
1801
  case "update_tool": {
1797
- const idx = config3.tools.confirmed.findIndex((t) => t.name === args.tool_name);
1802
+ const idx = config4.tools.confirmed.findIndex((t) => t.name === args.tool_name);
1798
1803
  if (idx === -1) {
1799
1804
  return errResult("not_found", `Tool "${args.tool_name}" not found in confirmed tools`);
1800
1805
  }
1801
- const existing = config3.tools.confirmed[idx];
1806
+ const existing = config4.tools.confirmed[idx];
1802
1807
  if (!existing) {
1803
1808
  return errResult("not_found", `Tool "${args.tool_name}" not found`);
1804
1809
  }
1805
- config3.tools.confirmed[idx] = {
1810
+ config4.tools.confirmed[idx] = {
1806
1811
  ...existing,
1807
1812
  ...data.version !== void 0 ? { version: data.version } : {},
1808
1813
  ...data.notes !== void 0 ? { notes: data.notes } : {},
1809
1814
  ...data.chosen_reason !== void 0 ? { chosen_reason: data.chosen_reason } : {},
1810
1815
  ...data.alternatives_considered !== void 0 ? { alternatives_considered: data.alternatives_considered } : {}
1811
1816
  };
1812
- config3.audit_log.push({
1817
+ config4.audit_log.push({
1813
1818
  action: "update_tool",
1814
1819
  tool: args.tool_name,
1815
1820
  timestamp: now,
@@ -1818,15 +1823,15 @@ async function handleUpdateProjectConfig(args) {
1818
1823
  break;
1819
1824
  }
1820
1825
  case "add_evaluation": {
1821
- if (!config3.tools.pending_evaluation.some((t) => t.name === args.tool_name) && !config3.tools.confirmed.some((t) => t.name === args.tool_name)) {
1826
+ if (!config4.tools.pending_evaluation.some((t) => t.name === args.tool_name) && !config4.tools.confirmed.some((t) => t.name === args.tool_name)) {
1822
1827
  const pending = {
1823
1828
  name: args.tool_name,
1824
1829
  category: data.category ?? "other",
1825
1830
  added_at: now
1826
1831
  };
1827
- config3.tools.pending_evaluation.push(pending);
1832
+ config4.tools.pending_evaluation.push(pending);
1828
1833
  }
1829
- config3.audit_log.push({
1834
+ config4.audit_log.push({
1830
1835
  action: "add_evaluation",
1831
1836
  tool: args.tool_name,
1832
1837
  timestamp: now,
@@ -1835,14 +1840,14 @@ async function handleUpdateProjectConfig(args) {
1835
1840
  break;
1836
1841
  }
1837
1842
  }
1838
- const updated_config_json = JSON.stringify(config3, null, 2);
1843
+ const updated_config_json = JSON.stringify(config4, null, 2);
1839
1844
  return okResult({
1840
1845
  updated_config_json,
1841
1846
  file_path: ".toolpilot/config.json",
1842
1847
  action_applied: args.action,
1843
1848
  tool_name: args.tool_name,
1844
- confirmed_count: config3.tools.confirmed.length,
1845
- pending_count: config3.tools.pending_evaluation.length,
1849
+ confirmed_count: config4.tools.confirmed.length,
1850
+ pending_count: config4.tools.pending_evaluation.length,
1846
1851
  instructions: "Write updated_config_json to .toolpilot/config.json to persist this change."
1847
1852
  });
1848
1853
  } catch (e) {
@@ -2245,13 +2250,87 @@ function createTransport() {
2245
2250
  // src/index.prod.ts
2246
2251
  process.env.TOOLPILOT_MODE = "production";
2247
2252
  var logger9 = pino9({ name: "@toolcairn/mcp-server" });
2253
+ function buildAuthGateServer() {
2254
+ const server = new McpServer2(
2255
+ { name: "toolcairn", version: "0.1.0" },
2256
+ {
2257
+ instructions: `
2258
+ # ToolCairn \u2014 Sign In Required
2259
+
2260
+ You are NOT authenticated. ToolCairn tools are unavailable until you sign in.
2261
+
2262
+ ## Action Required
2263
+
2264
+ Call: \`toolcairn_auth\` with \`{ "action": "login" }\`
2265
+
2266
+ This opens a browser window. The user signs in at toolcairn.neurynae.com/device,
2267
+ confirms the code, and the token is saved locally. After that, tell the user to
2268
+ restart their agent \u2014 all 14 tools will be available on the next session.
2269
+ `.trim()
2270
+ }
2271
+ );
2272
+ server.registerTool(
2273
+ "toolcairn_auth",
2274
+ {
2275
+ description: 'Sign in to ToolCairn. Required before any other tools are available. Call with action="login" to start.',
2276
+ inputSchema: z3.object({
2277
+ action: z3.enum(["login", "status"]).describe('"login" starts sign-in, "status" checks current state')
2278
+ })
2279
+ },
2280
+ async ({ action }) => {
2281
+ if (action === "status") {
2282
+ return {
2283
+ content: [
2284
+ {
2285
+ type: "text",
2286
+ text: JSON.stringify({
2287
+ authenticated: false,
2288
+ message: 'Not signed in. Call toolcairn_auth with action="login".'
2289
+ })
2290
+ }
2291
+ ]
2292
+ };
2293
+ }
2294
+ try {
2295
+ const user = await startDeviceAuth(import_config3.config.TOOLPILOT_API_URL);
2296
+ return {
2297
+ content: [
2298
+ {
2299
+ type: "text",
2300
+ text: JSON.stringify({
2301
+ ok: true,
2302
+ message: `Signed in as ${user.email}. Restart your agent \u2014 all ToolCairn tools will be available.`,
2303
+ user_email: user.email
2304
+ })
2305
+ }
2306
+ ]
2307
+ };
2308
+ } catch (err) {
2309
+ const msg = err instanceof Error ? err.message : "Authentication failed";
2310
+ return {
2311
+ content: [{ type: "text", text: JSON.stringify({ ok: false, error: msg }) }],
2312
+ isError: true
2313
+ };
2314
+ }
2315
+ }
2316
+ );
2317
+ return server;
2318
+ }
2248
2319
  async function main() {
2249
- logger9.info("Starting ToolCairn MCP Server (production mode)");
2250
2320
  await ensureProjectSetup();
2251
- const server = await buildProdServer();
2321
+ const creds = await loadCredentials();
2322
+ const authenticated = creds !== null && isTokenValid(creds);
2323
+ let server;
2324
+ if (authenticated) {
2325
+ logger9.info({ user: creds.user_email }, "ToolCairn MCP Server starting (authenticated)");
2326
+ server = await buildProdServer();
2327
+ } else {
2328
+ logger9.info("ToolCairn MCP Server starting (auth-gate mode \u2014 sign in required)");
2329
+ server = buildAuthGateServer();
2330
+ }
2252
2331
  const transport = createTransport();
2253
2332
  await server.connect(transport);
2254
- logger9.info("ToolCairn MCP Server started");
2333
+ logger9.info("ToolCairn MCP Server ready");
2255
2334
  }
2256
2335
  main().catch((error) => {
2257
2336
  pino9({ name: "@toolcairn/mcp-server" }).error({ err: error }, "Failed to start MCP server");