@cachly-dev/mcp-server 0.10.5 → 0.10.6

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.
Files changed (50) hide show
  1. package/.github/workflows/publish.yml +45 -0
  2. package/LAUNCH_TEXTS.md +182 -0
  3. package/LICENSE +202 -0
  4. package/dist/__tests__/auth.test.d.ts +1 -0
  5. package/dist/__tests__/auth.test.js +107 -0
  6. package/dist/__tests__/auth.test.js.map +1 -0
  7. package/dist/auth.d.ts +3 -0
  8. package/dist/auth.js +32 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/handlers/advanced.js +38 -31
  11. package/dist/handlers/advanced.js.map +1 -1
  12. package/dist/handlers/brain.js +144 -50
  13. package/dist/handlers/brain.js.map +1 -1
  14. package/dist/handlers/fedbrain.js +55 -38
  15. package/dist/handlers/fedbrain.js.map +1 -1
  16. package/dist/handlers/roadmap.js +8 -2
  17. package/dist/handlers/roadmap.js.map +1 -1
  18. package/dist/handlers/syndicate.js +15 -7
  19. package/dist/handlers/syndicate.js.map +1 -1
  20. package/dist/handlers/team.js +62 -50
  21. package/dist/handlers/team.js.map +1 -1
  22. package/dist/index.js +222 -217
  23. package/dist/index.js.map +1 -1
  24. package/dist/tools.d.ts +0 -8
  25. package/dist/tools.js +5 -15
  26. package/dist/tools.js.map +1 -1
  27. package/dist/utils.d.ts +1 -0
  28. package/dist/utils.js +13 -0
  29. package/dist/utils.js.map +1 -0
  30. package/glama.json +29 -0
  31. package/llms.txt +155 -0
  32. package/package.json +29 -13
  33. package/scripts/postinstall.js +12 -19
  34. package/server.json +3 -9
  35. package/smithery.yaml +6 -8
  36. package/src/__tests__/auth.test.ts +147 -0
  37. package/src/auth.ts +46 -0
  38. package/src/handlers/advanced.ts +37 -23
  39. package/src/handlers/brain.ts +141 -47
  40. package/src/handlers/fedbrain.ts +43 -31
  41. package/src/handlers/roadmap.ts +7 -2
  42. package/src/handlers/syndicate.ts +13 -7
  43. package/src/handlers/team.ts +57 -33
  44. package/src/index.ts +224 -234
  45. package/src/tools.ts +5 -16
  46. package/src/utils.ts +10 -0
  47. package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
  48. package/dist/__tests__/brain-predict-failures.test.js +0 -339
  49. package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
  50. package/src/__tests__/brain-predict-failures.test.ts +0 -430
package/llms.txt ADDED
@@ -0,0 +1,155 @@
1
+ # cachly AI Brain — MCP Server
2
+
3
+ > Persistent memory layer for AI coding assistants. Gives Claude Code, Cursor, GitHub Copilot, Windsurf, Cline and Zed a permanent brain that learns from every session, bug fix, git commit, and CI run — automatically.
4
+
5
+ ## What cachly is
6
+
7
+ cachly is an MCP (Model Context Protocol) server that adds long-term memory to AI coding assistants. Unlike conversation-window context, cachly memory persists forever across sessions, projects, and team members.
8
+
9
+ It works by maintaining a Causal Knowledge Graph (CKG) — a structured store of lessons, bug fixes, architecture decisions, deployment commands, and failure patterns. On every session start, the AI receives a briefing with relevant lessons. On every fix or commit, new lessons are automatically stored.
10
+
11
+ **Key differentiator**: causal_trace — given any error or problem description, traces the causal chain through the graph (root cause → intermediate causes → known fix). No other memory system does this.
12
+
13
+ ## Installation (zero config)
14
+
15
+ ```bash
16
+ npx @cachly-dev/mcp-server@latest setup
17
+ ```
18
+
19
+ This command: authenticates via OAuth device flow (one browser click), auto-detects all installed editors, writes the correct MCP config for each, creates CLAUDE.md with Brain rules, installs a git post-commit hook.
20
+
21
+ Free tier: 25 MB, no credit card, German servers (GDPR).
22
+
23
+ ## Supported editors
24
+
25
+ Claude Code, Cursor, Windsurf, VS Code, GitHub Copilot, Cline, Zed, Continue.dev
26
+
27
+ ## How memory works
28
+
29
+ 1. Session starts → AI receives: last session summary, open failures, relevant lessons, memory crystal (compressed history)
30
+ 2. During work → AI stores: WIP state, architecture decisions, file summaries
31
+ 3. Bug fixed → lesson stored: what failed, what worked, commands used, severity, file paths
32
+ 4. Editor closes → session summary saved, git log analyzed, ambient lessons extracted
33
+ 5. Next session → AI arrives pre-briefed
34
+
35
+ ## Tool catalog (89 tools)
36
+
37
+ ### Session & Memory
38
+ - session_start: Single-call session briefing — last summary, open failures, recent lessons, brain health
39
+ - session_end: Save session summary when finishing work
40
+ - session_handoff: Detailed handoff for next chat window
41
+ - session_ping: Lightweight checkpoint every ~5 tool calls
42
+ - auto_learn_session: Auto-learn from session observations without explicit calls
43
+ - sync_file_changes: Associate recent file changes with brain knowledge
44
+
45
+ ### Lesson Storage & Recall
46
+ - learn_from_attempts: Store structured lesson from any fix, deploy, or discovery
47
+ - recall_best_solution: Best known solution for a topic with success/failure history
48
+ - smart_recall: BM25+ semantic search across all brain data (11 languages: EN DE FR ES IT PT ZH JA KO AR HE)
49
+ - brain_search: Full-text search over all brain data
50
+ - recall_at: Brain archaeology — see what a lesson looked like at a point in time
51
+
52
+ ### Causal Analysis & Prediction
53
+ - causal_trace: Root Cause Analysis through memory — problem → causal chain → known fix
54
+ - brain_predict: Predictive pre-fetch — predicts likely failures given current context, returns fixes
55
+ - brain_predict_failures: Pre-deploy failure prediction with probability percentages
56
+ - trace_dependency: Find all lessons that depend on a given prerequisite
57
+ - knowledge_decay: Confidence scoring for every lesson
58
+
59
+ ### Context Management
60
+ - remember_context: Save architecture findings, decisions, file summaries
61
+ - recall_context: Retrieve saved context by key (supports glob)
62
+ - list_remembered: List all cached context entries
63
+ - forget_context: Delete cached context entries
64
+ - compact_recover: Reconstruct full context from Memory Crystal after context limit
65
+
66
+ ### Memory Maintenance
67
+ - memory_consolidate: Weekly garbage collector — deduplicates, detects contradictions, expires stale lessons
68
+ - memory_crystalize: Compress sessions into dense Memory Crystal snapshot
69
+ - crystal_view: Inspect current Memory Crystal
70
+ - brain_diff: See exactly what changed in the brain since a point in time
71
+ - brain_doctor: Health check — lesson count, IQ boost %, open failures, recommendations
72
+ - brain_from_git: Bootstrap brain lessons from existing git history
73
+
74
+ ### Team Brain
75
+ - team_learn: Store lesson in shared team brain
76
+ - team_recall: Recall from shared team brain (shows who learned what)
77
+ - team_synthesize: Merge multiple contributors' lessons on same topic into canonical version
78
+ - madc_deliberate: Multi-Agent Deliberation Chamber — 6 specialist agents vote on conflicting lessons
79
+
80
+ ### Knowledge Commons (Global)
81
+ - syndicate: Contribute verified lesson to global Knowledge Commons
82
+ - syndicate_search: Search global Knowledge Commons
83
+ - syndicate_trending: Trending lessons globally
84
+ - fedbrain_contribute: Contribute with cryptographic knowledge certificate
85
+ - fedbrain_search: Context-weighted search of global commons
86
+ - fedbrain_confirm: Confirm a syndicated lesson worked for you
87
+ - publish_lesson: Publish lesson to Cachly Public Brain
88
+ - import_public_brain: Import community lessons for a framework
89
+ - global_learn: Store lesson that applies across all your projects
90
+ - global_recall: Retrieve cross-project lessons
91
+
92
+ ### Continuous Learning Stream
93
+ - cls_ingest: Ingest learning signals from git commits, CI runs, IDE diagnostics without session_end
94
+ - cls_install_hooks: Generate git post-commit hook and GitHub Actions step for automatic learning
95
+
96
+ ### Autopilot — self-managing AI instructions
97
+ - autopilot: Generate a CLAUDE.md / copilot-instructions.md / .github/copilot-instructions.md from actual Brain content. Supports Claude, Cursor, Copilot, Windsurf, Gemini. style="minimal" for just the hooks, style="full" for complete ruleset with examples. Re-run as the Brain learns more to continuously upgrade the instructions file.
98
+
99
+ ### Roadmap & Planning
100
+ - roadmap_add: Add item to persistent project roadmap stored in Brain
101
+ - roadmap_update: Update roadmap item status, priority, details
102
+ - roadmap_list: List roadmap items filtered by status, priority, tag, milestone
103
+ - roadmap_next: Get the single most important next actionable item
104
+
105
+ ### Cache & Infrastructure
106
+ - list_instances / create_instance / delete_instance: Manage Brain instances
107
+ - cache_get / cache_set / cache_delete: Standard Redis/Valkey cache operations
108
+ - cache_mget / cache_mset: Bulk pipeline (single round-trip)
109
+ - cache_exists / cache_ttl / cache_keys / cache_stats: Cache introspection
110
+ - semantic_search: Find cached entries by semantic similarity
111
+ - cache_warmup: Pre-warm semantic cache with prompt/value pairs
112
+ - cache_lock_acquire / cache_lock_release: Distributed locking
113
+ - cache_stream_set / cache_stream_get: LLM token stream caching
114
+ - index_project: Index source files for semantic search
115
+ - detect_namespace: Classify prompts into semantic namespaces
116
+ - get_connection_string: Get Redis/Valkey connection string
117
+ - get_api_status: Full diagnostic — call when anything is broken
118
+
119
+ ### Organization & Teams
120
+ - list_orgs / create_org / invite_member / get_org_plan: Multi-tenant organization management
121
+ - ckg_inspect: Inspect Causal Knowledge Graph for a concept
122
+ - autopilot: Generate CLAUDE.md / copilot-instructions.md for self-managing AI
123
+ - setup_ai_memory: One-shot setup of 3-layer AI Memory system
124
+ - brain_federate: Private org knowledge transfer
125
+
126
+ ## Comparison to alternatives
127
+
128
+ | Feature | cachly | mem0 | Plain CLAUDE.md |
129
+ |---------|--------|------|-----------------|
130
+ | Persistent across sessions | ✅ | ✅ | Manual |
131
+ | Causal root cause analysis | ✅ | ❌ | ❌ |
132
+ | Automatic (no explicit calls) | ✅ | ❌ | ❌ |
133
+ | Team knowledge sharing | ✅ | Paid | ❌ |
134
+ | Failure prediction | ✅ | ❌ | ❌ |
135
+ | Git-ambient learning | ✅ | ❌ | ❌ |
136
+ | 11-language search | ✅ | ❌ | ❌ |
137
+ | Free tier forever | ✅ | Limited | ✅ |
138
+ | GDPR / EU servers | ✅ | ❌ | ✅ |
139
+
140
+ ## Pricing
141
+
142
+ - Free: 25 MB, 1 instance, €0/month forever
143
+ - Dev: 200 MB, €19/month
144
+ - Pro: 900 MB, team features, €49/month
145
+ - Speed: 900 MB + Dragonfly engine, €79/month
146
+ - Business: 7 GB, €199/month
147
+
148
+ All plans: German servers, GDPR-compliant, 99.9% SLA.
149
+
150
+ ## Links
151
+
152
+ - npm: https://www.npmjs.com/package/@cachly-dev/mcp-server
153
+ - GitHub: https://github.com/cachly-dev/cachly-mcp
154
+ - Docs: https://cachly.dev/docs/ai-memory
155
+ - Dashboard: https://cachly.dev
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "mcpName": "io.github.cachly-dev/cachly-mcp",
5
- "description": "Give Claude a persistent memory that never forgets. cachly learns automatically from every session, bug fix & git commit — no manual calls needed. Arrives pre-briefed. Predicts failures before they happen. One-click sign-in (OAuth Device Flow, no copy-paste) · 63 tools · Free tier · EU servers · Claude Code · Cursor · GitHub Copilot · Windsurf · Continue.",
5
+ "description": "Your AI forgets everything between sessions. cachly fixes that permanently. Persistent memory + causal root-cause analysis for Claude Code, Cursor, Copilot & Windsurf. Learns from every bug fix, git commit, and CI run automatically. Arrives pre-briefed. Predicts failures before they happen. 89 MCP tools · Free tier forever · GDPR · EU servers.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "cachly-mcp": "dist/index.js"
@@ -42,37 +42,53 @@
42
42
  },
43
43
  "keywords": [
44
44
  "mcp",
45
+ "mcp-server",
45
46
  "model-context-protocol",
47
+ "ai-memory",
48
+ "persistent-memory",
49
+ "claude-code-memory",
46
50
  "claude",
47
51
  "claude-code",
48
52
  "cursor",
53
+ "cursor-memory",
49
54
  "copilot",
50
55
  "github-copilot",
51
56
  "windsurf",
57
+ "cline",
58
+ "zed",
52
59
  "continue",
53
- "ai-memory",
54
60
  "ai-brain",
55
- "persistent-memory",
56
61
  "brain",
57
- "sessions",
58
- "lessons-learned",
59
- "semantic-cache",
62
+ "causal-knowledge-graph",
63
+ "causal-trace",
64
+ "root-cause-analysis",
65
+ "failure-prediction",
66
+ "brain-predict",
67
+ "smart-recall",
68
+ "memory-crystals",
60
69
  "team-knowledge",
61
70
  "ambient-git",
62
- "memory-crystals",
63
- "causal-knowledge-graph",
64
71
  "continuous-learning",
72
+ "lessons-learned",
73
+ "session-memory",
74
+ "semantic-cache",
75
+ "semantic-search",
65
76
  "predictive-ai",
77
+ "developer-productivity",
78
+ "knowledge-base",
79
+ "knowledge-graph",
80
+ "ai-context",
81
+ "context-window",
82
+ "long-term-memory",
83
+ "llm-memory",
66
84
  "cachly",
67
85
  "redis",
68
86
  "valkey",
69
87
  "cache",
70
88
  "ai",
71
89
  "llm",
72
- "knowledge-base",
73
- "developer-productivity",
74
- "ckg",
75
- "mcp-server"
90
+ "gdpr",
91
+ "ckg"
76
92
  ],
77
93
  "homepage": "https://cachly.dev/docs/mcp",
78
94
  "bugs": {
@@ -2,35 +2,28 @@
2
2
  // Shown once after: npm install @cachly-dev/mcp-server
3
3
 
4
4
  const c = {
5
- reset: "\x1b[0m",
6
- bold: "\x1b[1m",
5
+ reset: "\x1b[0m",
6
+ bold: "\x1b[1m",
7
7
  violet: "\x1b[35m",
8
- green: "\x1b[32m",
9
- cyan: "\x1b[36m",
8
+ green: "\x1b[32m",
9
+ cyan: "\x1b[36m",
10
+ dim: "\x1b[2m",
10
11
  yellow: "\x1b[33m",
11
- dim: "\x1b[2m",
12
12
  };
13
13
 
14
14
  const line = `${c.dim}─────────────────────────────────────────────────────${c.reset}`;
15
15
 
16
16
  console.log(`
17
17
  ${line}
18
- ${c.bold}${c.violet}🧠 Cachly Brain MCP Server${c.reset} ${c.dim}v${process.env.npm_package_version ?? ""}${c.reset}
18
+ ${c.bold}${c.violet}🧠 cachly AI Brain${c.reset} ${c.dim}v${process.env.npm_package_version ?? ""}${c.reset}
19
19
 
20
- ${c.bold}Give your AI assistant persistent memory in 30 seconds:${c.reset}
20
+ ${c.bold}One command sets everything up:${c.reset}
21
21
 
22
- ${c.cyan}1.${c.reset} Get a free Brain instance at ${c.bold}cachly.dev/setup-ai${c.reset}
23
- ${c.cyan}2.${c.reset} Add to Claude Code / Cursor / Copilot / Windsurf:
24
- ${c.dim}npx @cachly-dev/mcp-server@latest init${c.reset}
22
+ ${c.cyan}npx @cachly-dev/mcp-server@latest setup${c.reset}
25
23
 
26
- ${c.green}✅ Free tier · GDPR · German servers · No credit card${c.reset}
27
-
28
- ${c.dim}──────────────────────────────────────────────────────${c.reset}
29
- ${c.yellow}📦 Related packages:${c.reset}
30
- ${c.cyan}@cachly-dev/openclaw${c.reset} Cut LLM costs 60–90% in JS/TS apps
31
- ${c.dim}npm install @cachly-dev/openclaw${c.reset}
32
- ${c.cyan}cachly${c.reset} CLI to manage your Brain instances
33
- ${c.dim}npm install -g cachly${c.reset}
24
+ Signs you in → detects your editors → writes all MCP configs
25
+ automatically → adds Brain rules to CLAUDE.md.
34
26
 
27
+ ${c.green}No credentials. No credit card. Free forever.${c.reset}
28
+ ${c.dim}Other editors: https://cachly.dev/docs/mcp${c.reset}
35
29
  ${line}`);
36
-
package/server.json CHANGED
@@ -1,27 +1,21 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.cachly-dev/cachly-mcp",
4
- "description": "Give Claude a persistent memory that never forgets. cachly learns automatically from every session, bug fix, and git commit — no manual calls needed. Arrives pre-briefed. Predicts failures before they happen. One-click sign-in (OAuth Device Flow) · 63 tools · Free tier · EU servers · works with Claude Code, Cursor, Copilot, Windsurf.",
4
+ "description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 89 MCP tools · Free tier · EU servers · GDPR.",
5
5
  "repository": {
6
6
  "url": "https://github.com/cachly-dev/cachly-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.10.1",
9
+ "version": "0.10.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "name": "@cachly-dev/mcp-server",
14
14
  "identifier": "@cachly-dev/mcp-server",
15
- "version": "0.10.1",
15
+ "version": "0.10.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
19
19
  }
20
- ],
21
- "remotes": [
22
- {
23
- "transportType": "streamable-http",
24
- "url": "https://mcp.cachly.dev/mcp"
25
- }
26
20
  ]
27
21
  }
package/smithery.yaml CHANGED
@@ -5,23 +5,21 @@ startCommand:
5
5
  properties:
6
6
  CACHLY_JWT:
7
7
  type: string
8
- title: "API Key"
9
- description: "Optional. Leave blank — on your first tool call the server starts a one-click browser sign-in (OAuth Device Flow) and auto-provisions your free Brain instance. No copy-paste, no dashboard visit required. Or pre-fill with your API key from cachly.dev/setup-ai."
10
- default: ""
8
+ title: "API Key (optional — sign in via browser on first use)"
9
+ description: "Leave blank for zero-config: on your first tool call the server opens a browser sign-in (OAuth Device Flow) and auto-provisions your Brain instance. Or pre-fill with your API key from cachly.dev/setup-ai."
11
10
  examples:
12
11
  - "cky_live_abc123..."
13
12
  CACHLY_BRAIN_INSTANCE_ID:
14
13
  type: string
15
- title: "Brain Instance ID"
16
- description: "Optional. Leave blank — the server auto-detects your active instance. Only set this if you have multiple Brain instances."
17
- default: ""
14
+ title: "Brain Instance ID (optional)"
15
+ description: "Only needed if you have multiple Brain instances. Leave blank — the server auto-detects your active instance from the API key."
18
16
  pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
19
17
  examples:
20
18
  - "9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
21
19
  CACHLY_API_URL:
22
20
  type: string
23
- title: "API URL"
24
- description: "Optional. Leave blank unless you self-host cachly."
21
+ title: "API URL (optional)"
22
+ description: "Leave blank unless you self-host Cachly."
25
23
  default: "https://api.cachly.dev"
26
24
  required: []
27
25
  commandFunction: |-
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
3
+ import { jwtExpiryMs, checkJwt, handleApiError } from '../auth.js';
4
+
5
+ function makeJwt(payload: Record<string, unknown>): string {
6
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
7
+ const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
8
+ return `${header}.${body}.fakesig`;
9
+ }
10
+
11
+ const nowSec = () => Math.floor(Date.now() / 1000);
12
+
13
+ describe('jwtExpiryMs', () => {
14
+ it('returns null for an empty string', () => {
15
+ expect(jwtExpiryMs('')).toBeNull();
16
+ });
17
+
18
+ it('returns null for a plain (non-JWT) string', () => {
19
+ expect(jwtExpiryMs('not-a-token')).toBeNull();
20
+ });
21
+
22
+ it('returns null when the JWT has only two parts', () => {
23
+ expect(jwtExpiryMs('header.payload')).toBeNull();
24
+ });
25
+
26
+ it('returns null when the JWT has four parts', () => {
27
+ expect(jwtExpiryMs('a.b.c.d')).toBeNull();
28
+ });
29
+
30
+ it('returns null when the payload base64 is invalid', () => {
31
+ expect(jwtExpiryMs('header.!!!.sig')).toBeNull();
32
+ });
33
+
34
+ it('returns null when the payload has no exp field', () => {
35
+ expect(jwtExpiryMs(makeJwt({ sub: 'user-123', iat: nowSec() }))).toBeNull();
36
+ });
37
+
38
+ it('returns null when exp is 0', () => {
39
+ expect(jwtExpiryMs(makeJwt({ exp: 0 }))).toBeNull();
40
+ });
41
+
42
+ it('converts exp (seconds) to milliseconds', () => {
43
+ const expSec = nowSec() + 3600;
44
+ expect(jwtExpiryMs(makeJwt({ exp: expSec }))).toBe(expSec * 1000);
45
+ });
46
+
47
+ it('returns a past timestamp for an expired token', () => {
48
+ expect(jwtExpiryMs(makeJwt({ exp: nowSec() - 300 }))).toBeLessThan(Date.now());
49
+ });
50
+
51
+ it('returns a future timestamp for a valid token', () => {
52
+ expect(jwtExpiryMs(makeJwt({ exp: nowSec() + 86400 }))).toBeGreaterThan(Date.now());
53
+ });
54
+
55
+ it('handles a token expiring in less than 60 minutes', () => {
56
+ const expSec = nowSec() + 30 * 60;
57
+ const result = jwtExpiryMs(makeJwt({ exp: expSec }))!;
58
+ const minsLeft = Math.floor((result - Date.now()) / 60_000);
59
+ expect(minsLeft).toBeGreaterThanOrEqual(29);
60
+ expect(minsLeft).toBeLessThanOrEqual(30);
61
+ });
62
+
63
+ it('ignores extra claims', () => {
64
+ const expSec = nowSec() + 3600;
65
+ expect(jwtExpiryMs(makeJwt({ sub: 'u', iat: nowSec(), exp: expSec, plan: 'free' }))).toBe(expSec * 1000);
66
+ });
67
+
68
+ it('handles padding-free base64url payloads', () => {
69
+ // base64url (no =) — Buffer.from handles this natively
70
+ const expSec = nowSec() + 7200;
71
+ expect(jwtExpiryMs(makeJwt({ exp: expSec, x: 'a' }))).toBe(expSec * 1000);
72
+ });
73
+ });
74
+
75
+ describe('checkJwt', () => {
76
+ it('throws InvalidRequest when jwt is empty', () => {
77
+ expect(() => checkJwt('')).toThrowError(
78
+ expect.objectContaining({ code: ErrorCode.InvalidRequest, message: expect.stringContaining('CACHLY_JWT env var not set') }),
79
+ );
80
+ });
81
+
82
+ it('throws with setup URL when jwt is missing', () => {
83
+ expect(() => checkJwt('')).toThrowError(
84
+ expect.objectContaining({ message: expect.stringContaining('cachly.dev/setup-ai') }),
85
+ );
86
+ });
87
+
88
+ it('throws InvalidRequest with expiry timestamp for an expired token', () => {
89
+ const expSec = nowSec() - 60;
90
+ const token = makeJwt({ exp: expSec });
91
+ expect(() => checkJwt(token)).toThrowError(
92
+ expect.objectContaining({
93
+ code: ErrorCode.InvalidRequest,
94
+ message: expect.stringContaining('expired at'),
95
+ }),
96
+ );
97
+ });
98
+
99
+ it('does not throw for a valid non-expired token', () => {
100
+ expect(() => checkJwt(makeJwt({ exp: nowSec() + 3600 }))).not.toThrow();
101
+ });
102
+
103
+ it('does not throw for a token with no exp claim', () => {
104
+ expect(() => checkJwt(makeJwt({ sub: 'user' }))).not.toThrow();
105
+ });
106
+ });
107
+
108
+ describe('handleApiError', () => {
109
+ it('throws InvalidRequest for 401 with the detail message', () => {
110
+ expect(() => handleApiError(401, 'token expired')).toThrowError(
111
+ expect.objectContaining({
112
+ code: ErrorCode.InvalidRequest,
113
+ message: expect.stringContaining('Authentication failed (401): token expired'),
114
+ }),
115
+ );
116
+ });
117
+
118
+ it('throws InvalidRequest for 403 with the detail message', () => {
119
+ expect(() => handleApiError(403, 'plan limit reached')).toThrowError(
120
+ expect.objectContaining({
121
+ code: ErrorCode.InvalidRequest,
122
+ message: expect.stringContaining('Access denied (403): plan limit reached'),
123
+ }),
124
+ );
125
+ });
126
+
127
+ it('throws InternalError for other status codes', () => {
128
+ expect(() => handleApiError(500, 'server crash')).toThrowError(
129
+ expect.objectContaining({
130
+ code: ErrorCode.InternalError,
131
+ message: expect.stringContaining('cachly API error 500: server crash'),
132
+ }),
133
+ );
134
+ });
135
+
136
+ it('includes the cachly.dev/setup-ai link for 401 errors', () => {
137
+ expect(() => handleApiError(401, 'Unauthorized')).toThrowError(
138
+ expect.objectContaining({ message: expect.stringContaining('cachly.dev/setup-ai') }),
139
+ );
140
+ });
141
+
142
+ it('includes account access hint for 403 errors', () => {
143
+ expect(() => handleApiError(403, 'Forbidden')).toThrowError(
144
+ expect.objectContaining({ message: expect.stringContaining('access to this resource') }),
145
+ );
146
+ });
147
+ });
package/src/auth.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+
3
+ export function jwtExpiryMs(token: string): number | null {
4
+ try {
5
+ const parts = token.split('.');
6
+ if (parts.length !== 3) return null;
7
+ const payload = JSON.parse(
8
+ Buffer.from(parts[1], 'base64url').toString('utf-8'),
9
+ ) as { exp?: number };
10
+ return payload.exp ? payload.exp * 1000 : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function checkJwt(jwt: string): void {
17
+ if (!jwt) {
18
+ throw new McpError(
19
+ ErrorCode.InvalidRequest,
20
+ 'CACHLY_JWT env var not set.\n\nGet your token at https://cachly.dev/setup-ai and add it to your MCP config:\n CACHLY_JWT=<your-token>',
21
+ );
22
+ }
23
+ const expMs = jwtExpiryMs(jwt);
24
+ if (expMs !== null && expMs < Date.now()) {
25
+ throw new McpError(
26
+ ErrorCode.InvalidRequest,
27
+ `CACHLY_JWT expired at ${new Date(expMs).toISOString()}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`,
28
+ );
29
+ }
30
+ }
31
+
32
+ export function handleApiError(status: number, detail: string): never {
33
+ if (status === 401) {
34
+ throw new McpError(
35
+ ErrorCode.InvalidRequest,
36
+ `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`,
37
+ );
38
+ }
39
+ if (status === 403) {
40
+ throw new McpError(
41
+ ErrorCode.InvalidRequest,
42
+ `Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`,
43
+ );
44
+ }
45
+ throw new McpError(ErrorCode.InternalError, `cachly API error ${status}: ${detail}`);
46
+ }