@ainote/mcp 1.3.6 → 1.3.7

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/README-ko.md CHANGED
@@ -156,7 +156,7 @@ npm install
156
156
  export AINOTE_API_KEY="여기에-api-키-입력"
157
157
 
158
158
  # 선택사항 (기본값: 프로덕션 URL)
159
- export AINOTE_API_URL="https://ainote-5muq.onrender.com"
159
+ export AINOTE_API_URL="https://api.ainote.dev"
160
160
  ```
161
161
 
162
162
  ### 2. Claude Desktop 설정
@@ -177,7 +177,7 @@ Claude Desktop 설정 파일에 다음 내용을 추가하세요:
177
177
  ],
178
178
  "env": {
179
179
  "AINOTE_API_KEY": "YOUR_API_KEY",
180
- "AINOTE_API_URL": "https://ainote-5muq.onrender.com"
180
+ "AINOTE_API_URL": "https://api.ainote.dev"
181
181
  }
182
182
  }
183
183
  }
@@ -193,7 +193,7 @@ Claude Desktop 설정 파일에 다음 내용을 추가하세요:
193
193
  "command": "ainote-mcp",
194
194
  "env": {
195
195
  "AINOTE_API_KEY": "YOUR_API_KEY",
196
- "AINOTE_API_URL": "https://ainote-5muq.onrender.com"
196
+ "AINOTE_API_URL": "https://api.ainote.dev"
197
197
  }
198
198
  }
199
199
  }
package/README.md CHANGED
@@ -193,7 +193,7 @@ The fallback file is only used when no system keychain is available (e.g. headle
193
193
 
194
194
  ### Environment overrides
195
195
 
196
- - `AINOTE_API_URL` — backend URL (default `https://ainote-5muq.onrender.com`; `login`, `call`, and the MCP server all share it, since stored credentials are keyed by host)
196
+ - `AINOTE_API_URL` — backend URL (default `https://api.ainote.dev`; `login`, `call`, and the MCP server all share it, since stored credentials are keyed by host)
197
197
  - `AINOTE_API_KEY` — pre-provisioned MCP key. When set, the login flow is bypassed entirely and this key is used as-is.
198
198
 
199
199
  ### Headless / CI usage
@@ -437,7 +437,7 @@ By default the server listens on `http://localhost:3030` and exposes two endpoin
437
437
  Environment variable | Description | Default
438
438
  ---|---|---
439
439
  `AINOTE_API_KEY` | API key used for MCP key authentication | **required**
440
- `AINOTE_API_URL` | Target AI Note API base URL | `https://ainote-5muq.onrender.com`
440
+ `AINOTE_API_URL` | Target AI Note API base URL | `https://api.ainote.dev`
441
441
  `AINOTE_MCP_HTTP_PORT` | Local port for the SSE server | `3030`
442
442
  `AINOTE_MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed browser origins | _any_
443
443
  `AINOTE_MCP_ALLOWED_HOSTS` | Comma-separated list of allowed `Host` headers | _any_
@@ -26,6 +26,7 @@ import path from 'node:path';
26
26
  import os from 'node:os';
27
27
  import url from 'node:url';
28
28
  import { createRequire } from 'node:module';
29
+ import { DEFAULT_API_URL, LEGACY_API_HOSTS } from '../core/config-manager.js';
29
30
 
30
31
  const require = createRequire(import.meta.url);
31
32
 
@@ -188,6 +189,47 @@ async function keychainDelete(apiUrl, field) {
188
189
  * refreshToken?: string, expiresAt?: number, source: 'keychain'|'file'|'mixed'}>}
189
190
  */
190
191
  export async function getCredentials(apiUrl) {
192
+ const own = await readCredentials(apiUrl);
193
+ // Credentials saved while the default was a legacy host still count for the
194
+ // default host, so a host move does not force everyone to log in again.
195
+ // Merge per field, own values winning: a token refresh stores only the new
196
+ // access/refresh tokens under the current host, and the MCP key saved under
197
+ // the legacy host must stay visible after that.
198
+ let merged = own;
199
+ for (const legacyUrl of legacyUrlsFor(apiUrl)) {
200
+ const legacy = await readCredentials(legacyUrl);
201
+ if (!legacy) continue;
202
+ merged = merged ? mergeCredentials(merged, legacy) : legacy;
203
+ }
204
+ return merged;
205
+ }
206
+
207
+ function mergeCredentials(primary, fallback) {
208
+ const out = { ...primary };
209
+ for (const key of ['mcpKey', 'accessToken', 'refreshToken', 'expiresAt']) {
210
+ if (out[key] === undefined && fallback[key] !== undefined) out[key] = fallback[key];
211
+ }
212
+ if (out.source !== fallback.source) out.source = 'mixed';
213
+ return out;
214
+ }
215
+
216
+ /**
217
+ * Legacy API URLs whose stored credentials belong to `apiUrl`. Only the exact
218
+ * canonical HTTPS origin qualifies, so a plain-http or other-port URL on the
219
+ * same host never receives them.
220
+ */
221
+ function legacyUrlsFor(apiUrl) {
222
+ let origin;
223
+ try {
224
+ origin = new url.URL(apiUrl).origin;
225
+ } catch {
226
+ return [];
227
+ }
228
+ if (origin !== new url.URL(DEFAULT_API_URL).origin) return [];
229
+ return LEGACY_API_HOSTS.map((host) => `https://${host}`);
230
+ }
231
+
232
+ async function readCredentials(apiUrl) {
191
233
  const host = apiHost(apiUrl);
192
234
 
193
235
  const fileStore = readFileStore();
@@ -287,6 +329,15 @@ export async function setCredentials(apiUrl, creds) {
287
329
  * Best-effort: errors are swallowed and the caller can assume cleared state.
288
330
  */
289
331
  export async function clearCredentials(apiUrl) {
332
+ // Logging out of the current host also drops legacy-host credentials;
333
+ // otherwise getCredentials() would fall back to them and resurrect the login.
334
+ for (const legacyUrl of legacyUrlsFor(apiUrl)) {
335
+ await clearHostCredentials(legacyUrl);
336
+ }
337
+ await clearHostCredentials(apiUrl);
338
+ }
339
+
340
+ async function clearHostCredentials(apiUrl) {
290
341
  const host = apiHost(apiUrl);
291
342
 
292
343
  for (const field of FIELDS) {
@@ -6,17 +6,41 @@
6
6
  * `ainote-mcp login` are namespaced by API host in the token store, so any
7
7
  * divergence here makes the other commands look up the wrong host.
8
8
  *
9
- * (api.ainote.dev is currently a CNAME to this same Render service, but it is
10
- * a different token-store key, so the default must stay identical everywhere.)
9
+ * The backend moved off Render (2026-11); the old Render host now answers 503.
10
+ * api.ainote.dev is the canonical domain, matching the iOS and Android apps.
11
11
  */
12
- export const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
12
+ export const DEFAULT_API_URL = 'https://api.ainote.dev';
13
+
14
+ /**
15
+ * Hosts that used to serve the API. A URL on one of these is rewritten to
16
+ * DEFAULT_API_URL (path kept), so configs that pinned the old host via
17
+ * AINOTE_API_URL keep working, and the token store can still find
18
+ * credentials saved under them.
19
+ */
20
+ export const LEGACY_API_HOSTS = ['ainote-5muq.onrender.com'];
21
+
22
+ function rewriteLegacyHost(raw) {
23
+ let parsed;
24
+ try {
25
+ parsed = new URL(raw);
26
+ } catch {
27
+ return raw;
28
+ }
29
+ if (!LEGACY_API_HOSTS.includes(parsed.host)) return raw;
30
+ const target = new URL(DEFAULT_API_URL);
31
+ parsed.protocol = target.protocol;
32
+ parsed.host = target.host;
33
+ return parsed.toString();
34
+ }
13
35
 
14
36
  /**
15
37
  * Resolve the effective API base URL: AINOTE_API_URL env override, else the
16
- * default. Trailing slashes are stripped so "${url}/api/..." joins cleanly.
38
+ * default. A legacy host is rewritten to the default host. Trailing slashes
39
+ * are stripped so "${url}/api/..." joins cleanly.
17
40
  */
18
41
  export function resolveApiUrl(env = process.env) {
19
- return (env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
42
+ const raw = env.AINOTE_API_URL || DEFAULT_API_URL;
43
+ return rewriteLegacyHost(raw).replace(/\/+$/, '');
20
44
  }
21
45
 
22
46
  const DEFAULT_HTTP_PORT = 3030;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainote/mcp",
3
- "version": "1.3.6",
3
+ "version": "1.3.7",
4
4
  "mcpName": "io.github.seunghan91/ainote",
5
5
  "description": "MCP server for AI Note - Tasks, Dev Docs, and multi-device file sync (CLAUDE.md, memory files, Cursor rules, Windsurf rules)",
6
6
  "main": "index.js",