@neurynae/toolcairn-mcp 0.5.0 → 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 +478 -399
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
707
|
+
|
|
708
|
+
function renderAll() {
|
|
709
|
+
renderFeed();
|
|
710
|
+
renderMetrics();
|
|
711
|
+
renderToolChart();
|
|
712
|
+
renderInsights();
|
|
705
713
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
|
789
|
-
|
|
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
|
|
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(
|
|
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
|
|
1700
|
+
let config4;
|
|
1696
1701
|
try {
|
|
1697
|
-
|
|
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 (
|
|
1702
|
-
return errResult("version_error", `Unsupported config version: ${
|
|
1706
|
+
if (config4.version !== "1.0") {
|
|
1707
|
+
return errResult("version_error", `Unsupported config version: ${config4.version}`);
|
|
1703
1708
|
}
|
|
1704
|
-
const confirmedToolNames =
|
|
1705
|
-
const pendingToolNames =
|
|
1706
|
-
const staleTools =
|
|
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 =
|
|
1720
|
-
const toolpilot_indexed_tools =
|
|
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:
|
|
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:
|
|
1735
|
+
last_audit_entry: config4.audit_log.at(-1) ?? null,
|
|
1731
1736
|
agent_instructions: [
|
|
1732
|
-
`Project: ${
|
|
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
|
|
1757
|
+
let config4;
|
|
1753
1758
|
try {
|
|
1754
|
-
|
|
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
|
-
|
|
1763
|
-
if (!
|
|
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
|
-
|
|
1780
|
+
config4.tools.confirmed.push(newTool);
|
|
1776
1781
|
}
|
|
1777
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
1832
|
+
config4.tools.pending_evaluation.push(pending);
|
|
1828
1833
|
}
|
|
1829
|
-
|
|
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(
|
|
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:
|
|
1845
|
-
pending_count:
|
|
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
|
|
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
|
|
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");
|