@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 +365 -287
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
517
|
-
const configPath =
|
|
518
|
-
const trackerPath =
|
|
519
|
-
const eventsPath =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
1718
|
+
let config4;
|
|
1696
1719
|
try {
|
|
1697
|
-
|
|
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 (
|
|
1702
|
-
return errResult("version_error", `Unsupported config version: ${
|
|
1724
|
+
if (config4.version !== "1.0") {
|
|
1725
|
+
return errResult("version_error", `Unsupported config version: ${config4.version}`);
|
|
1703
1726
|
}
|
|
1704
|
-
const confirmedToolNames =
|
|
1705
|
-
const pendingToolNames =
|
|
1706
|
-
const staleTools =
|
|
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 =
|
|
1720
|
-
const toolpilot_indexed_tools =
|
|
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:
|
|
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:
|
|
1753
|
+
last_audit_entry: config4.audit_log.at(-1) ?? null,
|
|
1731
1754
|
agent_instructions: [
|
|
1732
|
-
`Project: ${
|
|
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
|
|
1775
|
+
let config4;
|
|
1753
1776
|
try {
|
|
1754
|
-
|
|
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
|
-
|
|
1763
|
-
if (!
|
|
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
|
-
|
|
1798
|
+
config4.tools.confirmed.push(newTool);
|
|
1776
1799
|
}
|
|
1777
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
1850
|
+
config4.tools.pending_evaluation.push(pending);
|
|
1828
1851
|
}
|
|
1829
|
-
|
|
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(
|
|
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:
|
|
1845
|
-
pending_count:
|
|
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
|
|
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
|
|
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");
|