@cachly-dev/mcp-server 0.10.47 → 0.10.48

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/src/index.ts CHANGED
@@ -2780,6 +2780,29 @@ if (!process.env.CACHLY_NO_UPDATE_CHECK) {
2780
2780
 
2781
2781
  const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
2782
2782
 
2783
+ // ── Per-request MCP Server factory (HTTP / Smithery stateless mode) ──────────
2784
+ // SDK ≥1.9 forbids reusing a stateless transport across requests.
2785
+ // Solution: create a fresh Server+Transport per /mcp request.
2786
+ // The module-level `server` below is used for stdio mode only.
2787
+ function createHttpMcpServer(): Server {
2788
+ const srv = new Server(
2789
+ { name: 'cachly-mcp', version: CURRENT_VERSION },
2790
+ { capabilities: { tools: {} } }
2791
+ );
2792
+ srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
2793
+ srv.setRequestHandler(CallToolRequestSchema, async (request) => {
2794
+ const { name, arguments: args } = request.params;
2795
+ try {
2796
+ const text = await handleTool(name, (args ?? {}) as Record<string, unknown>);
2797
+ return { content: [{ type: 'text', text }] };
2798
+ } catch (err) {
2799
+ if (err instanceof McpError) throw err;
2800
+ throw new McpError(ErrorCode.InternalError, (err as Error).message);
2801
+ }
2802
+ });
2803
+ return srv;
2804
+ }
2805
+
2783
2806
  if (httpPort) {
2784
2807
  // ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
2785
2808
  // Used for Smithery URL deployment: PORT=3000 node dist/index.js
@@ -2788,15 +2811,19 @@ if (httpPort) {
2788
2811
  // 2. Smithery config: ?config=base64url({CACHLY_JWT, CACHLY_BRAIN_INSTANCE_ID, ...})
2789
2812
  // Falls back to module-level env-derived values when neither is provided.
2790
2813
  const { createServer } = await import('node:http');
2791
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2792
- await server.connect(transport);
2793
2814
 
2794
2815
  function extractReqCtx(req: import('node:http').IncomingMessage, url: URL): ReqCtx {
2795
2816
  let jwtVal = '';
2796
2817
  let instId = '';
2797
2818
  const auth = req.headers['authorization'];
2798
- if (typeof auth === 'string' && auth.toLowerCase().startsWith('bearer ')) {
2799
- jwtVal = auth.slice(7).trim();
2819
+ if (typeof auth === 'string' && auth.trim()) {
2820
+ const a = auth.trim();
2821
+ jwtVal = a.toLowerCase().startsWith('bearer ') ? a.slice(7).trim() : a;
2822
+ }
2823
+ // Fallback: raw JWT in custom header (Smithery "header" transport without Bearer prefix)
2824
+ if (!jwtVal) {
2825
+ const rawJwt = req.headers['cachly_jwt'] ?? req.headers['cachly-jwt'] ?? req.headers['x-cachly-jwt'] ?? req.headers['x-api-key'];
2826
+ if (typeof rawJwt === 'string') jwtVal = rawJwt.trim();
2800
2827
  }
2801
2828
  const hdrInst = req.headers['x-cachly-instance-id'];
2802
2829
  if (typeof hdrInst === 'string') instId = hdrInst.trim();
@@ -2811,6 +2838,15 @@ if (httpPort) {
2811
2838
  if (!instId && typeof json.CACHLY_BRAIN_INSTANCE_ID === 'string') instId = json.CACHLY_BRAIN_INSTANCE_ID;
2812
2839
  } catch { /* malformed config — ignore, fall back to globals */ }
2813
2840
  }
2841
+ // Direct query params (Smithery "query" transport, manual clients, etc.)
2842
+ if (!jwtVal) {
2843
+ const q = url.searchParams.get('CACHLY_JWT') ?? url.searchParams.get('cachly_jwt') ?? url.searchParams.get('api_key');
2844
+ if (q) jwtVal = q.trim();
2845
+ }
2846
+ if (!instId) {
2847
+ const q = url.searchParams.get('CACHLY_BRAIN_INSTANCE_ID') ?? url.searchParams.get('cachly_brain_instance_id') ?? url.searchParams.get('instance_id');
2848
+ if (q) instId = q.trim();
2849
+ }
2814
2850
  return { jwt: jwtVal, instanceId: instId };
2815
2851
  }
2816
2852
 
@@ -2830,16 +2866,51 @@ if (httpPort) {
2830
2866
  }));
2831
2867
  return;
2832
2868
  }
2869
+ // Static server card — lets Smithery skip live scanning (avoids auth complexity).
2870
+ if (url.pathname === '/.well-known/mcp/server-card.json') {
2871
+ const card = {
2872
+ name: 'cachly AI Brain',
2873
+ version: CURRENT_VERSION,
2874
+ description: 'Persistent AI memory for Claude Code, Cursor, Copilot, Windsurf & friends. Remembers every lesson, fix, and architecture decision forever.',
2875
+ transport: { type: 'streamable-http', url: 'https://mcp.cachly.dev/mcp' },
2876
+ capabilities: { tools: {} },
2877
+ };
2878
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
2879
+ res.end(JSON.stringify(card));
2880
+ return;
2881
+ }
2833
2882
  if (url.pathname === '/mcp' || url.pathname === '/') {
2883
+ // Handle CORS preflight
2884
+ if (req.method === 'OPTIONS') {
2885
+ res.writeHead(204, {
2886
+ 'Access-Control-Allow-Origin': '*',
2887
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
2888
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Mcp-Session-Id, X-Cachly-Instance-Id, cachly_jwt, cachly-jwt, x-cachly-jwt, x-api-key',
2889
+ });
2890
+ res.end();
2891
+ return;
2892
+ }
2834
2893
  const ctx = extractReqCtx(req, url);
2835
- reqCtx.run(ctx, () => {
2836
- transport.handleRequest(req, res).catch((err) => {
2837
- process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
2838
- if (!res.headersSent) {
2839
- res.writeHead(500, { 'Content-Type': 'application/json' });
2840
- res.end(JSON.stringify({ error: 'internal_error' }));
2841
- }
2894
+ // Create a fresh Server+Transport for each request (stateless multi-tenant mode).
2895
+ // SDK ≥1.9 throws if a stateless transport is reused across requests.
2896
+ const reqServer = createHttpMcpServer();
2897
+ const reqTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
2898
+ reqServer.connect(reqTransport).then(() => {
2899
+ reqCtx.run(ctx, () => {
2900
+ reqTransport.handleRequest(req, res).catch((err) => {
2901
+ process.stderr.write(`cachly-mcp HTTP handler error: ${(err as Error).message}\n`);
2902
+ if (!res.headersSent) {
2903
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2904
+ res.end(JSON.stringify({ error: 'internal_error' }));
2905
+ }
2906
+ });
2842
2907
  });
2908
+ }).catch((err) => {
2909
+ process.stderr.write(`cachly-mcp connect error: ${(err as Error).message}\n`);
2910
+ if (!res.headersSent) {
2911
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2912
+ res.end(JSON.stringify({ error: 'internal_error' }));
2913
+ }
2843
2914
  });
2844
2915
  return;
2845
2916
  }
package/src/search.ts CHANGED
@@ -384,7 +384,7 @@ function hindiLightStem(word: string): string {
384
384
  * настройки → настройк (genitive plural)
385
385
  */
386
386
  function russianLightStem(word: string): string {
387
- let w = word;
387
+ const w = word;
388
388
  // Longest first to avoid partial stripping
389
389
  // Verb infinitives / gerunds
390
390
  if (w.endsWith('ывание') && w.length > 7) return w.slice(0, -6);
@@ -427,7 +427,7 @@ function russianLightStem(word: string): string {
427
427
  * yüklemek → yükle (-mek/-mak infinitive)
428
428
  */
429
429
  function turkishLightStem(word: string): string {
430
- let w = word;
430
+ const w = word;
431
431
  // Suffixes longest-first
432
432
  if (w.endsWith('lardaki') && w.length > 7) return w.slice(0, -7);
433
433
  if (w.endsWith('lerdeki') && w.length > 7) return w.slice(0, -7);
@@ -475,7 +475,7 @@ function turkishLightStem(word: string): string {
475
475
  * সমস্যাটি → সমস্যা (-টি singular marker)
476
476
  */
477
477
  function bengaliLightStem(word: string): string {
478
- let w = word;
478
+ const w = word;
479
479
  // Plural / collective markers (longest first)
480
480
  if (w.endsWith('\u0997\u09c1\u09b2\u09cb') && w.length > 5) return w.slice(0, -4); // গুলো (-gulo plural)
481
481
  if (w.endsWith('\u0997\u09c1\u09b2\u09bf') && w.length > 5) return w.slice(0, -4); // গুলি (-guli plural)