@cachly-dev/mcp-server 0.9.4 → 0.9.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.
package/dist/index.js CHANGED
@@ -45,9 +45,9 @@ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } f
45
45
  import { Redis } from 'ioredis';
46
46
  // ── Config ────────────────────────────────────────────────────────────────────
47
47
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
48
- const JWT = process.env.CACHLY_JWT ?? '';
48
+ let JWT = process.env.CACHLY_JWT ?? '';
49
49
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
50
- const CURRENT_VERSION = '0.9.4';
50
+ const CURRENT_VERSION = '0.9.6';
51
51
  // ── Default Instance Resolution (for Smithery & single-credential setups) ────
52
52
  // When CACHLY_BRAIN_INSTANCE_ID is set, tools can omit the instance_id parameter.
53
53
  // When neither is set, we auto-fetch the first running instance once per process.
@@ -78,6 +78,95 @@ async function resolveDefaultInstanceId() {
78
78
  catch { /* non-fatal — caller will surface missing instance_id error */ }
79
79
  return '';
80
80
  }
81
+ let _deviceFlow = null;
82
+ async function startDeviceFlow() {
83
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
84
+ const CLIENT_ID = 'cachly-cli';
85
+ try {
86
+ const res = await fetch(`${AUTH_BASE}/auth/device`, {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
89
+ body: `client_id=${CLIENT_ID}&scope=openid`,
90
+ signal: AbortSignal.timeout(8000),
91
+ });
92
+ if (!res.ok)
93
+ return null;
94
+ const data = await res.json();
95
+ return {
96
+ deviceCode: data.device_code,
97
+ userCode: data.user_code,
98
+ verifyUrl: data.verification_uri_complete,
99
+ pollInterval: (data.interval ?? 5) * 1000,
100
+ deadline: Date.now() + 10 * 60 * 1000, // 10 min
101
+ polling: false,
102
+ };
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ async function pollDeviceFlow(flow) {
109
+ if (Date.now() > flow.deadline)
110
+ return 'expired';
111
+ const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
112
+ const CLIENT_ID = 'cachly-cli';
113
+ try {
114
+ const res = await fetch(`${AUTH_BASE}/token`, {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
117
+ body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${flow.deviceCode}`,
118
+ signal: AbortSignal.timeout(8000),
119
+ });
120
+ const data = await res.json();
121
+ if (data.access_token) {
122
+ // Exchange Keycloak JWT → long-lived API key
123
+ let apiKey = data.access_token;
124
+ try {
125
+ const keyRes = await fetch(`${API_URL}/api/v1/api-keys`, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
128
+ body: JSON.stringify({ name: 'cachly-mcp-smithery', scope: 'read_write' }),
129
+ signal: AbortSignal.timeout(8000),
130
+ });
131
+ if (keyRes.ok) {
132
+ const keyBody = await keyRes.json();
133
+ if (keyBody.key)
134
+ apiKey = keyBody.key;
135
+ }
136
+ }
137
+ catch { /* use raw JWT as fallback */ }
138
+ JWT = apiKey;
139
+ _deviceFlow = null;
140
+ // Auto-provision instance
141
+ _defaultInstanceFetched = false;
142
+ await resolveDefaultInstanceId();
143
+ if (!_defaultInstanceId) {
144
+ // Try auto-provision
145
+ try {
146
+ const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
147
+ method: 'POST',
148
+ headers: { Authorization: `Bearer ${JWT}`, 'Content-Type': 'application/json' },
149
+ signal: AbortSignal.timeout(10000),
150
+ });
151
+ if (autoRes.ok) {
152
+ const body = await autoRes.json();
153
+ const id = body.instance?.id ?? body.instance_id;
154
+ if (id)
155
+ _defaultInstanceId = id;
156
+ }
157
+ }
158
+ catch { /* non-fatal */ }
159
+ }
160
+ return 'done';
161
+ }
162
+ if (data.error === 'slow_down')
163
+ flow.pollInterval = Math.min(flow.pollInterval + 2000, 15000);
164
+ return 'pending';
165
+ }
166
+ catch {
167
+ return 'pending';
168
+ }
169
+ }
81
170
  /**
82
171
  * ┌──────────────────────────────────────────────────────────────────────┐
83
172
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -3766,21 +3855,57 @@ async function handleTool(name, args) {
3766
3855
  // Guard: if no JWT, return actionable onboarding message instead of HTTP 401
3767
3856
  if (!JWT) {
3768
3857
  void sendAnonymousTelemetry(name);
3858
+ // ── Zero-credential device flow ─────────────────────────────────────
3859
+ // 1st call: start device flow, return code + URL
3860
+ // 2nd+ calls: poll for token; once authenticated, proceed transparently
3861
+ if (_deviceFlow) {
3862
+ const result = await pollDeviceFlow(_deviceFlow);
3863
+ if (result === 'done') {
3864
+ // Auth complete — re-enter handleTool with now-valid JWT
3865
+ return handleTool(name, args);
3866
+ }
3867
+ if (result === 'expired') {
3868
+ _deviceFlow = null;
3869
+ return '⌛ **Authentication timed out.** Please call any tool again to restart the sign-in flow.';
3870
+ }
3871
+ // Still pending
3872
+ return [
3873
+ '⏳ **Waiting for authentication...**',
3874
+ '',
3875
+ `Sign in at: **${_deviceFlow.verifyUrl}**`,
3876
+ `Enter code: **${_deviceFlow.userCode}**`,
3877
+ '',
3878
+ 'Once you complete sign-in in your browser, call this tool again and it will proceed automatically.',
3879
+ ].join('\n');
3880
+ }
3881
+ // No pending flow — start a new one
3882
+ const flow = await startDeviceFlow();
3883
+ if (flow) {
3884
+ _deviceFlow = flow;
3885
+ return [
3886
+ '🧠 **cachly AI Brain — One-click sign in**',
3887
+ '',
3888
+ '1. Open this URL in your browser (it may open automatically):',
3889
+ ` **${flow.verifyUrl}**`,
3890
+ '',
3891
+ `2. Enter this code if prompted: **${flow.userCode}**`,
3892
+ '',
3893
+ '3. After sign-in, call this tool again — it will proceed automatically.',
3894
+ '',
3895
+ '✨ Free tier includes: 1 Brain instance, persistent memory, 63 MCP tools.',
3896
+ ' No credit card required.',
3897
+ ].join('\n');
3898
+ }
3899
+ // Device flow unavailable (network issue) — fall back to manual setup
3769
3900
  return [
3770
3901
  '🧠 **cachly AI Brain — Setup required**',
3771
3902
  '',
3772
- 'You have no API credentials configured. Get started in 30 seconds:',
3773
- '',
3774
- '**Step 1:** Register for free (no credit card):',
3775
- ' 👉 https://cachly.dev/setup-ai',
3776
- '',
3777
- '**Step 2:** Run the setup wizard in your terminal:',
3903
+ 'Run the setup wizard once in your terminal:',
3778
3904
  ' ```',
3779
3905
  ' npx @cachly-dev/mcp-server@latest setup',
3780
3906
  ' ```',
3781
3907
  '',
3782
- 'The wizard auto-detects your editor (Claude Code, Cursor, Copilot, Windsurf)',
3783
- 'and writes the config file with your credentials.',
3908
+ 'Or get your API key at: https://cachly.dev/setup-ai',
3784
3909
  '',
3785
3910
  '✨ Free tier includes: 1 Brain instance, persistent memory, semantic search.',
3786
3911
  ].join('\n');