@neurynae/toolcairn-mcp 0.5.0 → 0.7.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,279 @@ 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 { spawn } = await import("child_process");
250
+ try {
251
+ const platform2 = process.platform;
252
+ let cmd;
253
+ let args;
254
+ if (platform2 === "win32") {
255
+ cmd = "cmd";
256
+ args = ["/c", "start", "", url];
257
+ } else if (platform2 === "darwin") {
258
+ cmd = "open";
259
+ args = [url];
260
+ } else {
261
+ cmd = "xdg-open";
262
+ args = [url];
263
+ }
264
+ const child = spawn(cmd, args, {
265
+ detached: true,
266
+ // detach from parent process group
267
+ stdio: "ignore",
268
+ // don't inherit parent's stdio
269
+ shell: false
270
+ });
271
+ child.unref();
272
+ } catch {
273
+ }
274
+ }
275
+ async function requestDeviceCode(apiUrl) {
276
+ const res = await fetch(`${apiUrl}/v1/auth/device-code`, { method: "POST" });
277
+ if (!res.ok)
278
+ throw new Error("Failed to start device auth. Check your internet connection.");
279
+ return await res.json();
280
+ }
281
+ async function startDeviceAuth(apiUrl) {
282
+ const codeData = await requestDeviceCode(apiUrl);
283
+ process.stderr.write("\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\n");
284
+ process.stderr.write(" ToolCairn \u2014 Sign In Required\n");
285
+ process.stderr.write("\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\n");
286
+ process.stderr.write("\n Opening browser for authentication...\n\n");
287
+ process.stderr.write(` URL: ${codeData.verification_uri}
288
+ `);
289
+ process.stderr.write(` Code: ${codeData.user_code}
290
+ `);
291
+ process.stderr.write("\n Waiting... (browser should open automatically)\n\n");
292
+ await openBrowser(codeData.verification_uri);
293
+ const result = await pollForToken(apiUrl, codeData.device_code, codeData.interval);
294
+ await upgradeToAuthenticated(result.access_token, result.api_key, result.user);
295
+ process.stderr.write(`
296
+ \u2713 Signed in as ${result.user.email}
297
+
298
+ `);
299
+ return {
300
+ userId: result.user.id,
301
+ email: result.user.email ?? "",
302
+ name: result.user.name
303
+ };
304
+ }
305
+ async function pollForToken(apiUrl, deviceCode, intervalSec) {
306
+ const intervalMs = Math.max(intervalSec, 5) * 1e3;
307
+ while (true) {
308
+ await sleep(intervalMs);
309
+ const res = await fetch(`${apiUrl}/v1/auth/token`, {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json" },
312
+ body: JSON.stringify({ device_code: deviceCode, grant_type: "device_code" })
313
+ });
314
+ const data = await res.json();
315
+ if (data.error === "authorization_pending")
316
+ continue;
317
+ if (data.error === "expired_token")
318
+ throw new Error("Device code expired. Please try again.");
319
+ if (data.error)
320
+ throw new Error(`Authorization failed: ${data.error}`);
321
+ if (data.access_token)
322
+ return data;
323
+ }
324
+ }
325
+ function sleep(ms) {
326
+ return new Promise((resolve) => setTimeout(resolve, ms));
327
+ }
328
+
329
+ // src/index.prod.ts
65
330
  import pino9 from "pino";
331
+ import { z as z3 } from "zod";
66
332
 
67
333
  // src/project-setup.ts
68
334
  init_esm_shims();
69
- import { access, mkdir, writeFile } from "fs/promises";
335
+ import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
70
336
  import { platform, type } from "os";
71
- import { join } from "path";
337
+ import { join as join2 } from "path";
72
338
  import pino from "pino";
73
339
 
74
340
  // src/tools/generate-tracker.ts
@@ -513,13 +779,13 @@ async function ensureProjectSetup(projectRoot = process.cwd()) {
513
779
  { os: os.label, platform: os.platform, projectRoot },
514
780
  "Detected OS \u2014 starting project setup"
515
781
  );
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");
782
+ const dir = join2(projectRoot, ".toolcairn");
783
+ const configPath = join2(dir, "config.json");
784
+ const trackerPath = join2(dir, "tracker.html");
785
+ const eventsPath = join2(dir, "events.jsonl");
520
786
  const eventsPathForUrl = toFileUrl(eventsPath);
521
787
  try {
522
- await mkdir(dir, { recursive: true });
788
+ await mkdir2(dir, { recursive: true });
523
789
  await createIfAbsent(configPath, JSON.stringify(INITIAL_CONFIG, null, 2), "config.json");
524
790
  await createIfAbsent(trackerPath, generateTrackerHtml(eventsPathForUrl), "tracker.html");
525
791
  await createIfAbsent(eventsPath, "", "events.jsonl");
@@ -536,7 +802,7 @@ async function createIfAbsent(filePath, content, label) {
536
802
  await access(filePath);
537
803
  logger.debug({ file: label }, "Already exists \u2014 skipping");
538
804
  } catch {
539
- await writeFile(filePath, content, "utf-8");
805
+ await writeFile2(filePath, content, "utf-8");
540
806
  logger.info({ file: label }, "Created");
541
807
  }
542
808
  }
@@ -546,249 +812,6 @@ init_esm_shims();
546
812
  var import_config = __toESM(require_dist(), 1);
547
813
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
548
814
 
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;
681
- }
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;
689
- }
690
- }
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;
701
- }
702
- async function saveCredentials(creds) {
703
- await mkdir2(CREDENTIALS_DIR, { recursive: true });
704
- await writeFile2(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), "utf-8");
705
- }
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
- });
717
- }
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
- });
725
- }
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
766
- };
767
- }
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;
786
- }
787
- }
788
- function sleep(ms) {
789
- return new Promise((resolve) => setTimeout(resolve, ms));
790
- }
791
-
792
815
  // ../../packages/tools/dist/local.js
793
816
  init_esm_shims();
794
817
 
@@ -1647,7 +1670,7 @@ async function handleInitProjectConfig(args) {
1647
1670
  chosen_reason: "Auto-detected from project files during toolpilot_init",
1648
1671
  alternatives_considered: []
1649
1672
  }));
1650
- const config3 = {
1673
+ const config4 = {
1651
1674
  version: "1.0",
1652
1675
  project: {
1653
1676
  name: args.project_name,
@@ -1667,7 +1690,7 @@ async function handleInitProjectConfig(args) {
1667
1690
  }
1668
1691
  ]
1669
1692
  };
1670
- const config_json = JSON.stringify(config3, null, 2);
1693
+ const config_json = JSON.stringify(config4, null, 2);
1671
1694
  return okResult({
1672
1695
  config_json,
1673
1696
  file_path: ".toolpilot/config.json",
@@ -1692,18 +1715,18 @@ function daysSince(isoDate) {
1692
1715
  async function handleReadProjectConfig(args) {
1693
1716
  try {
1694
1717
  logger5.info("read_project_config called");
1695
- let config3;
1718
+ let config4;
1696
1719
  try {
1697
- config3 = JSON.parse(args.config_content);
1720
+ config4 = JSON.parse(args.config_content);
1698
1721
  } catch {
1699
1722
  return errResult("parse_error", "config_content is not valid JSON");
1700
1723
  }
1701
- if (config3.version !== "1.0") {
1702
- return errResult("version_error", `Unsupported config version: ${config3.version}`);
1724
+ if (config4.version !== "1.0") {
1725
+ return errResult("version_error", `Unsupported config version: ${config4.version}`);
1703
1726
  }
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) => {
1727
+ const confirmedToolNames = config4.tools.confirmed.map((t) => t.name);
1728
+ const pendingToolNames = config4.tools.pending_evaluation.map((t) => t.name);
1729
+ const staleTools = config4.tools.confirmed.filter((t) => {
1707
1730
  const date = t.last_verified ?? t.chosen_at ?? t.confirmed_at;
1708
1731
  return date ? daysSince(date) > STALENESS_THRESHOLD_DAYS : true;
1709
1732
  }).map((t) => {
@@ -1716,10 +1739,10 @@ async function handleReadProjectConfig(args) {
1716
1739
  recommendation: "Consider using check_issue to verify no new known issues"
1717
1740
  };
1718
1741
  });
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);
1742
+ const non_oss_tools = config4.tools.confirmed.filter((t) => t.source === "non_oss").map((t) => t.name);
1743
+ const toolpilot_indexed_tools = config4.tools.confirmed.filter((t) => t.source === "toolpilot").map((t) => t.name);
1721
1744
  return okResult({
1722
- project: config3.project,
1745
+ project: config4.project,
1723
1746
  confirmed_tools: confirmedToolNames,
1724
1747
  pending_tools: pendingToolNames,
1725
1748
  non_oss_tools,
@@ -1727,9 +1750,9 @@ async function handleReadProjectConfig(args) {
1727
1750
  stale_tools: staleTools,
1728
1751
  total_confirmed: confirmedToolNames.length,
1729
1752
  total_pending: pendingToolNames.length,
1730
- last_audit_entry: config3.audit_log.at(-1) ?? null,
1753
+ last_audit_entry: config4.audit_log.at(-1) ?? null,
1731
1754
  agent_instructions: [
1732
- `Project: ${config3.project.name} (${config3.project.language}${config3.project.framework ? `, ${config3.project.framework}` : ""})`,
1755
+ `Project: ${config4.project.name} (${config4.project.language}${config4.project.framework ? `, ${config4.project.framework}` : ""})`,
1733
1756
  `Already confirmed tools: ${confirmedToolNames.join(", ") || "none"}`,
1734
1757
  "When recommending tools, skip any already in confirmed_tools.",
1735
1758
  non_oss_tools.length > 0 ? `Non-OSS tools in project (handle separately): ${non_oss_tools.join(", ")}` : "",
@@ -1749,9 +1772,9 @@ var logger6 = pino6({ name: "@toolpilot/tools:update-project-config" });
1749
1772
  async function handleUpdateProjectConfig(args) {
1750
1773
  try {
1751
1774
  logger6.info({ action: args.action, tool: args.tool_name }, "update_project_config called");
1752
- let config3;
1775
+ let config4;
1753
1776
  try {
1754
- config3 = JSON.parse(args.current_config);
1777
+ config4 = JSON.parse(args.current_config);
1755
1778
  } catch {
1756
1779
  return errResult("parse_error", "current_config is not valid JSON");
1757
1780
  }
@@ -1759,8 +1782,8 @@ async function handleUpdateProjectConfig(args) {
1759
1782
  const data = args.data ?? {};
1760
1783
  switch (args.action) {
1761
1784
  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)) {
1785
+ config4.tools.pending_evaluation = config4.tools.pending_evaluation.filter((t) => t.name !== args.tool_name);
1786
+ if (!config4.tools.confirmed.some((t) => t.name === args.tool_name)) {
1764
1787
  const newTool = {
1765
1788
  name: args.tool_name,
1766
1789
  source: data.source ?? "toolpilot",
@@ -1772,9 +1795,9 @@ async function handleUpdateProjectConfig(args) {
1772
1795
  query_id: data.query_id,
1773
1796
  notes: data.notes
1774
1797
  };
1775
- config3.tools.confirmed.push(newTool);
1798
+ config4.tools.confirmed.push(newTool);
1776
1799
  }
1777
- config3.audit_log.push({
1800
+ config4.audit_log.push({
1778
1801
  action: "add_tool",
1779
1802
  tool: args.tool_name,
1780
1803
  timestamp: now,
@@ -1783,9 +1806,9 @@ async function handleUpdateProjectConfig(args) {
1783
1806
  break;
1784
1807
  }
1785
1808
  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({
1809
+ config4.tools.confirmed = config4.tools.confirmed.filter((t) => t.name !== args.tool_name);
1810
+ config4.tools.pending_evaluation = config4.tools.pending_evaluation.filter((t) => t.name !== args.tool_name);
1811
+ config4.audit_log.push({
1789
1812
  action: "remove_tool",
1790
1813
  tool: args.tool_name,
1791
1814
  timestamp: now,
@@ -1794,22 +1817,22 @@ async function handleUpdateProjectConfig(args) {
1794
1817
  break;
1795
1818
  }
1796
1819
  case "update_tool": {
1797
- const idx = config3.tools.confirmed.findIndex((t) => t.name === args.tool_name);
1820
+ const idx = config4.tools.confirmed.findIndex((t) => t.name === args.tool_name);
1798
1821
  if (idx === -1) {
1799
1822
  return errResult("not_found", `Tool "${args.tool_name}" not found in confirmed tools`);
1800
1823
  }
1801
- const existing = config3.tools.confirmed[idx];
1824
+ const existing = config4.tools.confirmed[idx];
1802
1825
  if (!existing) {
1803
1826
  return errResult("not_found", `Tool "${args.tool_name}" not found`);
1804
1827
  }
1805
- config3.tools.confirmed[idx] = {
1828
+ config4.tools.confirmed[idx] = {
1806
1829
  ...existing,
1807
1830
  ...data.version !== void 0 ? { version: data.version } : {},
1808
1831
  ...data.notes !== void 0 ? { notes: data.notes } : {},
1809
1832
  ...data.chosen_reason !== void 0 ? { chosen_reason: data.chosen_reason } : {},
1810
1833
  ...data.alternatives_considered !== void 0 ? { alternatives_considered: data.alternatives_considered } : {}
1811
1834
  };
1812
- config3.audit_log.push({
1835
+ config4.audit_log.push({
1813
1836
  action: "update_tool",
1814
1837
  tool: args.tool_name,
1815
1838
  timestamp: now,
@@ -1818,15 +1841,15 @@ async function handleUpdateProjectConfig(args) {
1818
1841
  break;
1819
1842
  }
1820
1843
  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)) {
1844
+ if (!config4.tools.pending_evaluation.some((t) => t.name === args.tool_name) && !config4.tools.confirmed.some((t) => t.name === args.tool_name)) {
1822
1845
  const pending = {
1823
1846
  name: args.tool_name,
1824
1847
  category: data.category ?? "other",
1825
1848
  added_at: now
1826
1849
  };
1827
- config3.tools.pending_evaluation.push(pending);
1850
+ config4.tools.pending_evaluation.push(pending);
1828
1851
  }
1829
- config3.audit_log.push({
1852
+ config4.audit_log.push({
1830
1853
  action: "add_evaluation",
1831
1854
  tool: args.tool_name,
1832
1855
  timestamp: now,
@@ -1835,14 +1858,14 @@ async function handleUpdateProjectConfig(args) {
1835
1858
  break;
1836
1859
  }
1837
1860
  }
1838
- const updated_config_json = JSON.stringify(config3, null, 2);
1861
+ const updated_config_json = JSON.stringify(config4, null, 2);
1839
1862
  return okResult({
1840
1863
  updated_config_json,
1841
1864
  file_path: ".toolpilot/config.json",
1842
1865
  action_applied: args.action,
1843
1866
  tool_name: args.tool_name,
1844
- confirmed_count: config3.tools.confirmed.length,
1845
- pending_count: config3.tools.pending_evaluation.length,
1867
+ confirmed_count: config4.tools.confirmed.length,
1868
+ pending_count: config4.tools.pending_evaluation.length,
1846
1869
  instructions: "Write updated_config_json to .toolpilot/config.json to persist this change."
1847
1870
  });
1848
1871
  } catch (e) {
@@ -2246,12 +2269,67 @@ function createTransport() {
2246
2269
  process.env.TOOLPILOT_MODE = "production";
2247
2270
  var logger9 = pino9({ name: "@toolcairn/mcp-server" });
2248
2271
  async function main() {
2249
- logger9.info("Starting ToolCairn MCP Server (production mode)");
2250
2272
  await ensureProjectSetup();
2251
- const server = await buildProdServer();
2273
+ const creds = await loadCredentials();
2274
+ const authenticated = creds !== null && isTokenValid(creds);
2275
+ let server;
2276
+ if (authenticated) {
2277
+ logger9.info({ user: creds.user_email }, "Authenticated \u2014 starting full server");
2278
+ server = await buildProdServer();
2279
+ } else {
2280
+ let verificationUri = "https://toolcairn.neurynae.com/signup";
2281
+ let userCode = "";
2282
+ try {
2283
+ const codeData = await requestDeviceCode(import_config3.config.TOOLPILOT_API_URL);
2284
+ verificationUri = codeData.verification_uri;
2285
+ userCode = codeData.user_code;
2286
+ startDeviceAuth(import_config3.config.TOOLPILOT_API_URL).then(() => {
2287
+ logger9.info("Sign-in complete. Restart your agent to access all ToolCairn tools.");
2288
+ }).catch((err) => {
2289
+ logger9.error({ err }, "Sign-in failed \u2014 restart your agent and try again");
2290
+ });
2291
+ } catch (err) {
2292
+ logger9.error({ err }, "Could not reach ToolCairn API \u2014 check your connection");
2293
+ }
2294
+ server = new McpServer2(
2295
+ { name: "toolcairn", version: "0.1.0" },
2296
+ {
2297
+ instructions: userCode ? `# ToolCairn \u2014 Sign In Required
2298
+
2299
+ A browser window should have opened automatically.
2300
+
2301
+ **Sign-in URL:** ${verificationUri}
2302
+ **Code to confirm:** \`${userCode}\`
2303
+
2304
+ Open the URL, sign in, and confirm the code shown on the page. Then restart your agent \u2014 all 14 tools will be available.` : `# ToolCairn \u2014 Sign In Required
2305
+
2306
+ Visit ${verificationUri} to create an account, then restart your agent.`
2307
+ }
2308
+ );
2309
+ server.registerTool(
2310
+ "toolcairn_auth",
2311
+ {
2312
+ description: "Check ToolCairn sign-in status.",
2313
+ inputSchema: z3.object({ action: z3.enum(["status"]) })
2314
+ },
2315
+ async () => ({
2316
+ content: [
2317
+ {
2318
+ type: "text",
2319
+ text: JSON.stringify({
2320
+ authenticated: false,
2321
+ sign_in_url: verificationUri,
2322
+ code: userCode || null,
2323
+ message: userCode ? `Open ${verificationUri}, confirm code "${userCode}", then restart your agent.` : "Visit toolcairn.neurynae.com to create an account, then restart your agent."
2324
+ })
2325
+ }
2326
+ ]
2327
+ })
2328
+ );
2329
+ }
2252
2330
  const transport = createTransport();
2253
2331
  await server.connect(transport);
2254
- logger9.info("ToolCairn MCP Server started");
2332
+ logger9.info(authenticated ? "ToolCairn MCP ready" : "ToolCairn MCP ready (sign-in required)");
2255
2333
  }
2256
2334
  main().catch((error) => {
2257
2335
  pino9({ name: "@toolcairn/mcp-server" }).error({ err: error }, "Failed to start MCP server");