@cachly-dev/mcp-server 0.10.76 → 0.10.77

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/src/index.js CHANGED
@@ -60,10 +60,12 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
60
60
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
61
61
  import { Redis } from 'ioredis';
62
62
  // ── Config ────────────────────────────────────────────────────────────────────
63
- const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
63
+ // Mutable so the `setup`/`init` CLI can honor a `--api-url` flag for self-hosting.
64
+ // Reassigned (if at all) before any network call is made.
65
+ let API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
64
66
  let JWT = process.env.CACHLY_JWT ?? '';
65
67
  const _EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
66
- const CURRENT_VERSION = '0.10.76';
68
+ const CURRENT_VERSION = '0.10.77';
67
69
  // Max time to wait for a freshly-provisioned instance to become "running" before
68
70
  // giving up. Free-tier provisioning in high-latency regions can take 45–90s, so the
69
71
  // old hard-coded 25s caused spurious "instance_not_reachable" on first run.
@@ -1110,13 +1112,26 @@ const EDITOR_FILES = {
1110
1112
  };
1111
1113
  const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
1112
1114
  const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
1115
+ const DEFAULT_API_URL = 'https://api.cachly.dev';
1116
+ // Build the env block written into an editor's MCP config. Self-hosting is
1117
+ // first-class: if the operator runs setup/init against their own backend
1118
+ // (CACHLY_API_URL set to a non-default URL), that URL is baked into the config so
1119
+ // the editor-launched server talks to the self-hosted instance — not api.cachly.dev.
1120
+ // For the default cloud backend we OMIT CACHLY_API_URL to keep configs clean (the
1121
+ // binary already defaults to it).
1122
+ function buildServerEnv(apiKey, instanceId) {
1123
+ const env = { CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId };
1124
+ if (API_URL && API_URL !== DEFAULT_API_URL)
1125
+ env.CACHLY_API_URL = API_URL;
1126
+ return env;
1127
+ }
1113
1128
  function buildMcpConfig(apiKey, instanceId, editor) {
1114
1129
  if (editor === 'continue') {
1115
1130
  return JSON.stringify({
1116
1131
  experimental: {
1117
1132
  modelContextProtocolServers: [{
1118
1133
  transport: { type: 'stdio', command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'] },
1119
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1134
+ env: buildServerEnv(apiKey, instanceId),
1120
1135
  }],
1121
1136
  },
1122
1137
  }, null, 2);
@@ -1128,7 +1143,7 @@ function buildMcpConfig(apiKey, instanceId, editor) {
1128
1143
  command: {
1129
1144
  path: 'npx',
1130
1145
  args: ['-y', '@cachly-dev/mcp-server@latest'],
1131
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1146
+ env: buildServerEnv(apiKey, instanceId),
1132
1147
  },
1133
1148
  settings: {},
1134
1149
  },
@@ -1140,7 +1155,7 @@ function buildMcpConfig(apiKey, instanceId, editor) {
1140
1155
  cachly: {
1141
1156
  command: 'npx',
1142
1157
  args: ['-y', '@cachly-dev/mcp-server@latest'],
1143
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1158
+ env: buildServerEnv(apiKey, instanceId),
1144
1159
  },
1145
1160
  },
1146
1161
  }, null, 2);
@@ -1151,7 +1166,7 @@ async function mergeMcpConfig(configPath, apiKey, instanceId, editor, fsOps) {
1151
1166
  const cachlyEntry = {
1152
1167
  command: 'npx',
1153
1168
  args: ['-y', '@cachly-dev/mcp-server@latest'],
1154
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
1169
+ env: buildServerEnv(apiKey, instanceId),
1155
1170
  };
1156
1171
  if (!fsOps.existsSync(configPath))
1157
1172
  return buildMcpConfig(apiKey, instanceId, editor);
@@ -1558,7 +1573,9 @@ if (process.argv[2] === 'join') {
1558
1573
  env['CACHLY_BRAIN_INSTANCE_ID'] = instance_id;
1559
1574
  if (joinJwt && !env['CACHLY_JWT'])
1560
1575
  env['CACHLY_JWT'] = joinJwt;
1561
- env['CACHLY_API_URL'] ??= 'https://api.cachly.dev';
1576
+ // Only persist a self-hosted backend URL; leave default-cloud configs clean.
1577
+ if (API_URL !== DEFAULT_API_URL)
1578
+ env['CACHLY_API_URL'] = API_URL;
1562
1579
  servers['cachly'] = { ...existing, command: 'npx', args: ['-y', '@cachly-dev/mcp-server@latest'], env };
1563
1580
  cfg['mcpServers'] = servers;
1564
1581
  await wfJ(ePath, JSON.stringify(cfg, null, 2), 'utf-8');
@@ -1923,6 +1940,11 @@ if (process.argv[2] === 'init') {
1923
1940
  let apiKey = flag('api-key') ?? process.env.CACHLY_JWT;
1924
1941
  const editor = (flag('editor') ?? 'claude').toLowerCase();
1925
1942
  const projectDir = resolve(flag('project-dir') ?? '.');
1943
+ // Self-hosting: --api-url overrides the backend baked into the written config.
1944
+ const apiUrlFlag = flag('api-url');
1945
+ if (apiUrlFlag) {
1946
+ API_URL = apiUrlFlag.replace(/\/+$/, '');
1947
+ }
1926
1948
  // Zero-arg path: fall back to credentials saved by a previous `setup`
1927
1949
  // (~/.claude/mcp.json). Makes `init` a fast, idempotent re-configuration command
1928
1950
  // you can run in any project without copy-pasting tokens.
@@ -1944,7 +1966,7 @@ if (process.argv[2] === 'init') {
1944
1966
  catch { /* fall through to the usage error below */ }
1945
1967
  }
1946
1968
  if (!instanceId || !apiKey) {
1947
- console.error('\nUsage: npx @cachly-dev/mcp-server@latest init --instance-id <uuid> --api-key <cky_live_...> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path]\n');
1969
+ console.error('\nUsage: npx @cachly-dev/mcp-server@latest init --instance-id <uuid> --api-key <cky_live_...> [--editor claude|cursor|windsurf|copilot|continue] [--project-dir /path] [--api-url https://your-self-hosted-backend]\n');
1948
1970
  console.error('First time? Run the zero-config wizard (signs you in, auto-provisions): npx @cachly-dev/mcp-server@latest setup\n');
1949
1971
  console.error('Get your credentials from: https://cachly.dev/setup-ai\n');
1950
1972
  process.exit(1);
@@ -2108,6 +2130,11 @@ if (process.argv[2] === 'health') {
2108
2130
  if (!jwt) {
2109
2131
  fail('CACHLY_JWT not set — run: npx @cachly-dev/mcp-server@latest setup');
2110
2132
  }
2133
+ else if (jwt.startsWith('cky_')) {
2134
+ // Long-lived API key (the credential `setup` provisions). It is not a JWT and
2135
+ // never expires client-side — treat a well-formed key as healthy.
2136
+ ok('Long-lived API key set (cky_…) — no client-side expiry');
2137
+ }
2111
2138
  else {
2112
2139
  try {
2113
2140
  const parts = jwt.split('.');
@@ -2127,7 +2154,7 @@ if (process.argv[2] === 'health') {
2127
2154
  }
2128
2155
  }
2129
2156
  catch {
2130
- fail('CACHLY_JWT format invalid (expected JWT with 3 parts)');
2157
+ fail('CACHLY_JWT format invalid (expected a cky_ API key or a 3-part JWT)');
2131
2158
  }
2132
2159
  }
2133
2160
  // ── 2. Brain API reachability ───────────────────────────────────────────────
@@ -2275,6 +2302,30 @@ if (process.argv[2] === 'health') {
2275
2302
  else {
2276
2303
  warn('.git/hooks/post-commit not found — run: npx @cachly-dev/mcp-server@latest setup');
2277
2304
  }
2305
+ // ── 6. Embedding provider (BYOK) ─────────────────────────────────────────────
2306
+ console.log('\n🧬 Embedding provider (semantic search)');
2307
+ {
2308
+ const providerKeys = [
2309
+ ['OPENAI_API_KEY', 'openai'], ['GEMINI_API_KEY', 'gemini'],
2310
+ ['MISTRAL_API_KEY', 'mistral'], ['COHERE_API_KEY', 'cohere'],
2311
+ ['OLLAMA_BASE_URL', 'ollama'],
2312
+ ];
2313
+ const explicit = (process.env.CACHLY_EMBED_PROVIDER ?? '').toLowerCase();
2314
+ const byok = providerKeys.find(([envVar]) => process.env[envVar]);
2315
+ if (explicit && explicit !== 'cachly') {
2316
+ const envVar = providerKeys.find(([, p]) => p === explicit)?.[0];
2317
+ if (envVar && process.env[envVar])
2318
+ ok(`BYOK: ${explicit} (key present in ${envVar})`);
2319
+ else
2320
+ warn(`CACHLY_EMBED_PROVIDER=${explicit} but its key env var is not set — semantic search will fail`);
2321
+ }
2322
+ else if (byok) {
2323
+ ok(`BYOK: ${byok[1]} auto-detected (${byok[0]} present)`);
2324
+ }
2325
+ else {
2326
+ ok('Server-side embeddings (cachly) — no key needed, uses your JWT');
2327
+ }
2328
+ }
2278
2329
  // ── Summary ─────────────────────────────────────────────────────────────────
2279
2330
  console.log(`\n${'─'.repeat(40)}`);
2280
2331
  if (failed === 0) {
@@ -2295,6 +2346,13 @@ if (process.argv[2] === 'setup') {
2295
2346
  const { resolve, dirname } = await import('node:path');
2296
2347
  const { createInterface } = await import('node:readline');
2297
2348
  const setupStartMs = Date.now();
2349
+ // Self-hosting: `setup --api-url https://cachly.mycorp.internal` points the whole
2350
+ // wizard (auth, provisioning, config writes) at a private backend.
2351
+ const _apiUrlIdx = process.argv.indexOf('--api-url');
2352
+ if (_apiUrlIdx !== -1 && process.argv[_apiUrlIdx + 1]) {
2353
+ API_URL = process.argv[_apiUrlIdx + 1].replace(/\/+$/, '');
2354
+ console.log(`ℹ️ Using self-hosted backend: ${API_URL}\n`);
2355
+ }
2298
2356
  sendFunnelEvent('setup_started');
2299
2357
  // --yes / -y → non-interactive mode (skips all prompts, picks defaults).
2300
2358
  // Also auto-detect a non-interactive stdin (VSCode tasks, CI, piped input):
@@ -2656,7 +2714,7 @@ if (process.argv[2] === 'setup') {
2656
2714
  globalConfig.mcpServers['cachly'] = {
2657
2715
  command: 'npx',
2658
2716
  args: ['-y', '@cachly-dev/mcp-server@latest'],
2659
- env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: token, CACHLY_BRAIN_INSTANCE_ID: instance.id },
2717
+ env: buildServerEnv(token, instance.id),
2660
2718
  };
2661
2719
  await writeFile(globalClaudePath, JSON.stringify(globalConfig, null, 2), 'utf-8');
2662
2720
  console.log(`✅ Written: ~/.claude/mcp.json (global — works in every project)`);
@@ -2932,37 +2990,45 @@ if (process.argv[2] === 'learn-git') {
2932
2990
  process.exit(0);
2933
2991
  }
2934
2992
  // ── Start ─────────────────────────────────────────────────────────────────────
2935
- // Warn on stderr when credentials are missing so the user sees a clear
2936
- // actionable message in their editor's MCP log instead of silent failures.
2937
- // Skip for CLI commands that intentionally run without credentials.
2993
+ // Credential-missing handling at startup. The critical distinction is HOW we were
2994
+ // launched, because writing anything to stdout in stdio-MCP mode corrupts the
2995
+ // JSON-RPC stream and exiting kills the zero-credential device-flow onboarding.
2996
+ // • Human in a real terminal (TTY), no args → show the setup banner, exit 0.
2997
+ // • Editor as an MCP stdio server (non-TTY) → ONE stderr hint, then KEEP RUNNING
2998
+ // so tools/list works and the first tool call starts the browser sign-in.
2999
+ // Skip entirely for CLI subcommands that intentionally run without credentials.
2938
3000
  const _cliNoAuthCommands = ['demo', 'share', 'health', 'setup', 'init', 'digest', 'invite', 'badge', 'join', 'upgrade'];
2939
- if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '') && !(!process.argv[2] && process.stdout.isTTY)) {
2940
- // No args + no JWT in a non-TTY context = running as MCP server without credentials.
2941
- // Print actionable setup banner to stdout so it's captured by callers/tests.
2942
- const banner = '\n' +
2943
- '╔══════════════════════════════════════════════════════════════════╗\n' +
2944
- '║ 🧠 cachly AI Brain — Setup required ║\n' +
2945
- '╠══════════════════════════════════════════════════════════════════╣\n' +
2946
- '║ ║\n' +
2947
- '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
2948
- '║ ║\n' +
2949
- '║ 👉 https://cachly.dev/setup-ai ║\n' +
2950
- '║ ║\n' +
2951
- '║ Then run the interactive setup wizard: ║\n' +
2952
- '║ ║\n' +
2953
- '║ npx @cachly-dev/mcp-server@latest setup ║\n' +
2954
- '║ ║\n' +
2955
- '║ Free tier — no credit card required. ║\n' +
2956
- '╚══════════════════════════════════════════════════════════════════╝\n' +
2957
- '\n';
2958
- // When invoked with no args as the main entry point, write to stdout + exit cleanly.
2959
- // When used as MCP server (args present), write to stderr so editors see it.
2960
- if (!process.argv[2] && _isMain) {
3001
+ if (!JWT && !_cliNoAuthCommands.includes(process.argv[2] ?? '')) {
3002
+ const runningInTerminal = !process.argv[2] && process.stdout.isTTY === true && _isMain;
3003
+ if (runningInTerminal) {
3004
+ // A human ran `npx @cachly-dev/mcp-server` directly with no credentials.
3005
+ // Show them how to set up, then exit cleanly.
3006
+ const banner = '\n' +
3007
+ '╔══════════════════════════════════════════════════════════════════╗\n' +
3008
+ '║ 🧠 cachly AI Brain — Setup required ║\n' +
3009
+ '╠══════════════════════════════════════════════════════════════════╣\n' +
3010
+ '║ ║\n' +
3011
+ '║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
3012
+ '║ ║\n' +
3013
+ '║ 👉 https://cachly.dev/setup-ai ║\n' +
3014
+ '║ ║\n' +
3015
+ '║ Then run the interactive setup wizard: ║\n' +
3016
+ '║ ║\n' +
3017
+ '║ npx @cachly-dev/mcp-server@latest setup ║\n' +
3018
+ '║ ║\n' +
3019
+ '║ Free tier — no credit card required. ║\n' +
3020
+ '╚══════════════════════════════════════════════════════════════════╝\n' +
3021
+ '\n';
2961
3022
  process.stdout.write(banner);
2962
3023
  process.exit(0);
2963
3024
  }
2964
3025
  else {
2965
- process.stderr.write(banner);
3026
+ // Launched as an MCP stdio server (or HTTP) without credentials. NEVER touch
3027
+ // stdout here — it carries the JSON-RPC protocol. Emit a single actionable
3028
+ // hint to stderr (shown in the editor's MCP log) and fall through so the
3029
+ // server starts and the zero-credential device flow can run on first tool call.
3030
+ process.stderr.write('\n🧠 cachly: no CACHLY_JWT set yet — call any cachly tool and a 10-second browser sign-in starts automatically.\n' +
3031
+ ' (Or run once: npx @cachly-dev/mcp-server@latest setup)\n\n');
2966
3032
  }
2967
3033
  }
2968
3034
  else {