@neurynae/toolcairn-mcp 0.7.0 → 0.8.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
@@ -184,6 +184,31 @@ import { homedir } from "os";
184
184
  import { join } from "path";
185
185
  var CREDENTIALS_DIR = join(homedir(), ".toolcairn");
186
186
  var CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
187
+ var PENDING_AUTH_FILE = join(CREDENTIALS_DIR, "pending-auth.json");
188
+ async function savePendingAuth(data) {
189
+ await mkdir(CREDENTIALS_DIR, { recursive: true });
190
+ await writeFile(PENDING_AUTH_FILE, JSON.stringify(data, null, 2), "utf-8");
191
+ }
192
+ async function loadPendingAuth() {
193
+ try {
194
+ const raw = await readFile(PENDING_AUTH_FILE, "utf-8");
195
+ const data = JSON.parse(raw);
196
+ if (new Date(data.expires_at) < /* @__PURE__ */ new Date()) {
197
+ await clearPendingAuth();
198
+ return null;
199
+ }
200
+ return data;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ async function clearPendingAuth() {
206
+ try {
207
+ const { unlink } = await import("fs/promises");
208
+ await unlink(PENDING_AUTH_FILE);
209
+ } catch {
210
+ }
211
+ }
187
212
  function isTokenValid(creds) {
188
213
  if (!creds.access_token)
189
214
  return false;
@@ -261,13 +286,7 @@ async function openBrowser(url) {
261
286
  cmd = "xdg-open";
262
287
  args = [url];
263
288
  }
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
- });
289
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", shell: false });
271
290
  child.unref();
272
291
  } catch {
273
292
  }
@@ -276,21 +295,48 @@ async function requestDeviceCode(apiUrl) {
276
295
  const res = await fetch(`${apiUrl}/v1/auth/device-code`, { method: "POST" });
277
296
  if (!res.ok)
278
297
  throw new Error("Failed to start device auth. Check your internet connection.");
279
- return await res.json();
298
+ const data = await res.json();
299
+ await savePendingAuth({
300
+ device_code: data.device_code,
301
+ user_code: data.user_code,
302
+ verification_uri: data.verification_uri,
303
+ expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString(),
304
+ api_url: apiUrl
305
+ });
306
+ return data;
280
307
  }
281
308
  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}
309
+ const pending = await loadPendingAuth();
310
+ let codeData;
311
+ if (pending && pending.api_url === apiUrl) {
312
+ codeData = {
313
+ device_code: pending.device_code,
314
+ user_code: pending.user_code,
315
+ verification_uri: pending.verification_uri,
316
+ expires_in: Math.floor((new Date(pending.expires_at).getTime() - Date.now()) / 1e3),
317
+ interval: 5
318
+ };
319
+ process.stderr.write("\n ToolCairn: Waiting for sign-in confirmation...\n");
320
+ process.stderr.write(` URL: ${codeData.verification_uri}
321
+ `);
322
+ process.stderr.write(` Code: ${codeData.user_code}
323
+
324
+ `);
325
+ } else {
326
+ codeData = await requestDeviceCode(apiUrl);
327
+ 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");
328
+ process.stderr.write(" ToolCairn \u2014 Sign In Required\n");
329
+ 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");
330
+ process.stderr.write("\n Opening browser for authentication...\n\n");
331
+ process.stderr.write(` URL: ${codeData.verification_uri}
288
332
  `);
289
- process.stderr.write(` Code: ${codeData.user_code}
333
+ process.stderr.write(` Code: ${codeData.user_code}
290
334
  `);
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);
335
+ process.stderr.write("\n Waiting... (browser should open automatically)\n\n");
336
+ await openBrowser(codeData.verification_uri);
337
+ }
338
+ const result = await pollForToken(apiUrl, codeData.device_code, 5);
339
+ await clearPendingAuth();
294
340
  await upgradeToAuthenticated(result.access_token, result.api_key, result.user);
295
341
  process.stderr.write(`
296
342
  \u2713 Signed in as ${result.user.email}
@@ -314,8 +360,10 @@ async function pollForToken(apiUrl, deviceCode, intervalSec) {
314
360
  const data = await res.json();
315
361
  if (data.error === "authorization_pending")
316
362
  continue;
317
- if (data.error === "expired_token")
363
+ if (data.error === "expired_token") {
364
+ await clearPendingAuth();
318
365
  throw new Error("Device code expired. Please try again.");
366
+ }
319
367
  if (data.error)
320
368
  throw new Error(`Authorization failed: ${data.error}`);
321
369
  if (data.access_token)
@@ -810,62 +858,61 @@ async function createIfAbsent(filePath, content, label) {
810
858
  // src/server.prod.ts
811
859
  init_esm_shims();
812
860
  var import_config = __toESM(require_dist(), 1);
813
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
814
861
 
815
862
  // ../../packages/tools/dist/local.js
816
863
  init_esm_shims();
817
864
 
818
865
  // ../../packages/tools/dist/schemas.js
819
866
  init_esm_shims();
820
- import { z as z2 } from "zod";
867
+ import { z } from "zod";
821
868
  var searchToolsSchema = {
822
- query: z2.string().min(1).max(500),
823
- context: z2.object({ filters: z2.record(z2.string(), z2.unknown()) }).optional(),
824
- query_id: z2.string().uuid().optional(),
825
- user_id: z2.string().optional()
869
+ query: z.string().min(1).max(500),
870
+ context: z.object({ filters: z.record(z.string(), z.unknown()) }).optional(),
871
+ query_id: z.string().uuid().optional(),
872
+ user_id: z.string().optional()
826
873
  };
827
874
  var searchToolsRespondSchema = {
828
- query_id: z2.string().uuid(),
829
- answers: z2.array(z2.object({ dimension: z2.string(), value: z2.string() }))
875
+ query_id: z.string().uuid(),
876
+ answers: z.array(z.object({ dimension: z.string(), value: z.string() }))
830
877
  };
831
878
  var reportOutcomeSchema = {
832
- query_id: z2.string().uuid(),
833
- chosen_tool: z2.string(),
834
- reason: z2.string().optional(),
835
- outcome: z2.enum(["success", "failure", "replaced", "pending"]),
836
- feedback: z2.string().optional(),
837
- replaced_by: z2.string().optional()
879
+ query_id: z.string().uuid(),
880
+ chosen_tool: z.string(),
881
+ reason: z.string().optional(),
882
+ outcome: z.enum(["success", "failure", "replaced", "pending"]),
883
+ feedback: z.string().optional(),
884
+ replaced_by: z.string().optional()
838
885
  };
839
886
  var getStackSchema = {
840
- use_case: z2.string().min(1),
841
- constraints: z2.object({
842
- deployment_model: z2.enum(["self-hosted", "cloud", "embedded", "serverless"]).optional(),
843
- language: z2.string().optional(),
844
- license: z2.string().optional()
887
+ use_case: z.string().min(1),
888
+ constraints: z.object({
889
+ deployment_model: z.enum(["self-hosted", "cloud", "embedded", "serverless"]).optional(),
890
+ language: z.string().optional(),
891
+ license: z.string().optional()
845
892
  }).optional(),
846
- limit: z2.number().int().positive().max(10).default(5)
893
+ limit: z.number().int().positive().max(10).default(5)
847
894
  };
848
895
  var checkIssueSchema = {
849
- tool_name: z2.string(),
850
- issue_title: z2.string(),
851
- retry_count: z2.number().int().min(0).default(0),
852
- docs_consulted: z2.boolean().default(false),
853
- issue_url: z2.string().url().optional()
896
+ tool_name: z.string(),
897
+ issue_title: z.string(),
898
+ retry_count: z.number().int().min(0).default(0),
899
+ docs_consulted: z.boolean().default(false),
900
+ issue_url: z.string().url().optional()
854
901
  };
855
902
  var checkCompatibilitySchema = {
856
- tool_a: z2.string(),
857
- tool_b: z2.string()
903
+ tool_a: z.string(),
904
+ tool_b: z.string()
858
905
  };
859
906
  var suggestGraphUpdateSchema = {
860
- suggestion_type: z2.enum(["new_tool", "new_edge", "update_health", "new_use_case"]),
861
- data: z2.object({
862
- tool_name: z2.string().optional(),
863
- github_url: z2.string().url().optional(),
864
- description: z2.string().optional(),
865
- relationship: z2.object({
866
- source_tool: z2.string(),
867
- target_tool: z2.string(),
868
- edge_type: z2.enum([
907
+ suggestion_type: z.enum(["new_tool", "new_edge", "update_health", "new_use_case"]),
908
+ data: z.object({
909
+ tool_name: z.string().optional(),
910
+ github_url: z.string().url().optional(),
911
+ description: z.string().optional(),
912
+ relationship: z.object({
913
+ source_tool: z.string(),
914
+ target_tool: z.string(),
915
+ edge_type: z.enum([
869
916
  "SOLVES",
870
917
  "REQUIRES",
871
918
  "INTEGRATES_WITH",
@@ -875,68 +922,68 @@ var suggestGraphUpdateSchema = {
875
922
  "BREAKS_FROM",
876
923
  "COMPATIBLE_WITH"
877
924
  ]),
878
- evidence: z2.string().optional()
925
+ evidence: z.string().optional()
879
926
  }).optional(),
880
- use_case: z2.object({
881
- name: z2.string(),
882
- description: z2.string(),
883
- tools: z2.array(z2.string()).optional()
927
+ use_case: z.object({
928
+ name: z.string(),
929
+ description: z.string(),
930
+ tools: z.array(z.string()).optional()
884
931
  }).optional()
885
932
  }),
886
- query_id: z2.string().uuid().optional(),
887
- confidence: z2.number().min(0).max(1).default(0.5)
933
+ query_id: z.string().uuid().optional(),
934
+ confidence: z.number().min(0).max(1).default(0.5)
888
935
  };
889
936
  var compareToolsSchema = {
890
- tool_a: z2.string().min(1),
891
- tool_b: z2.string().min(1),
892
- use_case: z2.string().optional(),
893
- project_config: z2.string().max(1e5).optional()
937
+ tool_a: z.string().min(1),
938
+ tool_b: z.string().min(1),
939
+ use_case: z.string().optional(),
940
+ project_config: z.string().max(1e5).optional()
894
941
  };
895
942
  var toolpilotInitSchema = {
896
- agent: z2.enum(["claude", "cursor", "windsurf", "copilot", "copilot-cli", "opencode", "generic"]),
897
- project_root: z2.string().min(1),
898
- server_path: z2.string().optional(),
899
- detected_files: z2.array(z2.string()).optional()
943
+ agent: z.enum(["claude", "cursor", "windsurf", "copilot", "copilot-cli", "opencode", "generic"]),
944
+ project_root: z.string().min(1),
945
+ server_path: z.string().optional(),
946
+ detected_files: z.array(z.string()).optional()
900
947
  };
901
948
  var initProjectConfigSchema = {
902
- project_name: z2.string().min(1).max(200),
903
- language: z2.string().min(1).max(50),
904
- framework: z2.string().optional(),
905
- detected_tools: z2.array(z2.object({
906
- name: z2.string(),
907
- source: z2.enum(["toolpilot", "manual", "non_oss"]),
908
- version: z2.string().optional()
949
+ project_name: z.string().min(1).max(200),
950
+ language: z.string().min(1).max(50),
951
+ framework: z.string().optional(),
952
+ detected_tools: z.array(z.object({
953
+ name: z.string(),
954
+ source: z.enum(["toolpilot", "manual", "non_oss"]),
955
+ version: z.string().optional()
909
956
  })).optional()
910
957
  };
911
958
  var readProjectConfigSchema = {
912
- config_content: z2.string().min(1).max(1e5)
959
+ config_content: z.string().min(1).max(1e5)
913
960
  };
914
961
  var updateProjectConfigSchema = {
915
- current_config: z2.string().min(1).max(1e5),
916
- action: z2.enum(["add_tool", "remove_tool", "update_tool", "add_evaluation"]),
917
- tool_name: z2.string().min(1),
918
- data: z2.record(z2.string(), z2.unknown()).optional()
962
+ current_config: z.string().min(1).max(1e5),
963
+ action: z.enum(["add_tool", "remove_tool", "update_tool", "add_evaluation"]),
964
+ tool_name: z.string().min(1),
965
+ data: z.record(z.string(), z.unknown()).optional()
919
966
  };
920
967
  var classifyPromptSchema = {
921
- prompt: z2.string().min(1).max(2e3),
922
- project_tools: z2.array(z2.string()).optional()
968
+ prompt: z.string().min(1).max(2e3),
969
+ project_tools: z.array(z.string()).optional()
923
970
  };
924
971
  var verifySuggestionSchema = {
925
- query: z2.string().min(1).max(500),
926
- agent_suggestions: z2.array(z2.string().min(1)).min(1).max(10)
972
+ query: z.string().min(1).max(500),
973
+ agent_suggestions: z.array(z.string().min(1)).min(1).max(10)
927
974
  };
928
975
  var refineRequirementSchema = {
929
- prompt: z2.string().min(1).max(2e3),
930
- classification: z2.enum([
976
+ prompt: z.string().min(1).max(2e3),
977
+ classification: z.enum([
931
978
  "tool_discovery",
932
979
  "stack_building",
933
980
  "tool_comparison",
934
981
  "tool_configuration"
935
982
  ]),
936
- project_context: z2.object({
937
- existing_tools: z2.array(z2.string()).optional(),
938
- language: z2.string().optional(),
939
- framework: z2.string().optional()
983
+ project_context: z.object({
984
+ existing_tools: z.array(z.string()).optional(),
985
+ language: z.string().optional(),
986
+ framework: z.string().optional()
940
987
  }).optional()
941
988
  };
942
989
 
@@ -1876,6 +1923,7 @@ async function handleUpdateProjectConfig(args) {
1876
1923
 
1877
1924
  // src/server.prod.ts
1878
1925
  import pino8 from "pino";
1926
+ import { z as z2 } from "zod";
1879
1927
 
1880
1928
  // src/middleware/event-logger.ts
1881
1929
  init_esm_shims();
@@ -2041,27 +2089,17 @@ The server wrote the file at startup. You still need to fill in the project deta
2041
2089
  | A tool worked well or was replaced | \`report_outcome\` |
2042
2090
  | Tool added/removed from project | \`update_project_config\` |
2043
2091
  `.trim();
2044
- async function buildProdServer() {
2092
+ async function addToolsToServer(server) {
2045
2093
  const creds = await loadCredentials();
2046
2094
  if (!creds || !isTokenValid(creds)) {
2047
- throw new Error("ToolCairn: authentication required. Restart your agent to sign in.");
2095
+ throw new Error("ToolCairn: authentication required.");
2048
2096
  }
2049
2097
  const remote = new ToolCairnClient({
2050
2098
  baseUrl: import_config.config.TOOLPILOT_API_URL,
2051
2099
  apiKey: creds.client_id,
2052
2100
  accessToken: creds.access_token
2053
2101
  });
2054
- logger8.info(
2055
- {
2056
- apiUrl: import_config.config.TOOLPILOT_API_URL,
2057
- user: creds.user_email
2058
- },
2059
- "Production MCP mode: authenticated"
2060
- );
2061
- const server = new McpServer(
2062
- { name: "toolcairn", version: "0.1.0" },
2063
- { instructions: SETUP_INSTRUCTIONS }
2064
- );
2102
+ logger8.info({ user: creds.user_email }, "Registering production tools");
2065
2103
  server.registerTool(
2066
2104
  "classify_prompt",
2067
2105
  {
@@ -2186,8 +2224,8 @@ async function buildProdServer() {
2186
2224
  "toolcairn_auth",
2187
2225
  {
2188
2226
  description: 'Manage your ToolCairn authentication. Use "login" to authenticate via browser (unlocks higher rate limits), "status" to check current auth state, or "logout" to revert to anonymous mode.',
2189
- inputSchema: z.object({
2190
- action: z.enum(["login", "status", "logout"]).describe(
2227
+ inputSchema: z2.object({
2228
+ action: z2.enum(["login", "status", "logout"]).describe(
2191
2229
  '"login" opens a browser to authenticate, "status" shows current auth state, "logout" clears authentication'
2192
2230
  )
2193
2231
  })
@@ -2248,6 +2286,13 @@ async function buildProdServer() {
2248
2286
  }
2249
2287
  }
2250
2288
  );
2289
+ }
2290
+ async function buildProdServer() {
2291
+ const server = new McpServer(
2292
+ { name: "toolcairn", version: "0.1.0" },
2293
+ { instructions: SETUP_INSTRUCTIONS }
2294
+ );
2295
+ await addToolsToServer(server);
2251
2296
  return server;
2252
2297
  }
2253
2298
 
@@ -2280,32 +2325,29 @@ async function main() {
2280
2325
  let verificationUri = "https://toolcairn.neurynae.com/signup";
2281
2326
  let userCode = "";
2282
2327
  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
- });
2328
+ const pending = await loadPendingAuth();
2329
+ if (pending) {
2330
+ verificationUri = pending.verification_uri;
2331
+ userCode = pending.user_code;
2332
+ logger9.info({ userCode }, "Resuming pending sign-in");
2333
+ } else {
2334
+ const codeData = await requestDeviceCode(import_config3.config.TOOLPILOT_API_URL);
2335
+ verificationUri = codeData.verification_uri;
2336
+ userCode = codeData.user_code;
2337
+ logger9.info({ userCode }, "New sign-in started");
2338
+ }
2291
2339
  } catch (err) {
2292
2340
  logger9.error({ err }, "Could not reach ToolCairn API \u2014 check your connection");
2293
2341
  }
2294
- server = new McpServer2(
2295
- { name: "toolcairn", version: "0.1.0" },
2296
- {
2297
- instructions: userCode ? `# ToolCairn \u2014 Sign In Required
2342
+ const instructions = userCode ? `# ToolCairn \u2014 Sign In Required
2298
2343
 
2299
2344
  A browser window should have opened automatically.
2300
2345
 
2301
2346
  **Sign-in URL:** ${verificationUri}
2302
2347
  **Code to confirm:** \`${userCode}\`
2303
2348
 
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
- );
2349
+ Open the URL, sign in, and confirm the code shown. All 14 tools will appear automatically \u2014 no restart needed.` : "# ToolCairn \u2014 Sign In Required\n\nVisit https://toolcairn.neurynae.com to create an account, then reconnect.";
2350
+ server = new McpServer2({ name: "toolcairn", version: "0.1.0" }, { instructions });
2309
2351
  server.registerTool(
2310
2352
  "toolcairn_auth",
2311
2353
  {
@@ -2320,16 +2362,27 @@ Visit ${verificationUri} to create an account, then restart your agent.`
2320
2362
  authenticated: false,
2321
2363
  sign_in_url: verificationUri,
2322
2364
  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."
2365
+ message: userCode ? `Open ${verificationUri} and confirm code "${userCode}". Tools will appear automatically when confirmed.` : "Visit toolcairn.neurynae.com to sign up."
2324
2366
  })
2325
2367
  }
2326
2368
  ]
2327
2369
  })
2328
2370
  );
2371
+ startDeviceAuth(import_config3.config.TOOLPILOT_API_URL).then(async () => {
2372
+ logger9.info("Sign-in complete \u2014 adding all tools to running server");
2373
+ try {
2374
+ await addToolsToServer(server);
2375
+ logger9.info("All ToolCairn tools now available");
2376
+ } catch (err) {
2377
+ logger9.error({ err }, "Failed to add tools after sign-in \u2014 please reconnect");
2378
+ }
2379
+ }).catch((err) => {
2380
+ logger9.error({ err }, "Sign-in failed \u2014 please try again");
2381
+ });
2329
2382
  }
2330
2383
  const transport = createTransport();
2331
2384
  await server.connect(transport);
2332
- logger9.info(authenticated ? "ToolCairn MCP ready" : "ToolCairn MCP ready (sign-in required)");
2385
+ logger9.info(authenticated ? "ToolCairn MCP ready" : "ToolCairn MCP ready (awaiting sign-in)");
2333
2386
  }
2334
2387
  main().catch((error) => {
2335
2388
  pino9({ name: "@toolcairn/mcp-server" }).error({ err: error }, "Failed to start MCP server");