@ainote/mcp 1.3.3 → 1.3.4

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.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://api.ainote.dev`)
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)
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://api.ainote.dev`
440
+ `AINOTE_API_URL` | Target AI Note API base URL | `https://ainote-5muq.onrender.com`
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_
@@ -29,7 +29,8 @@ export async function authenticateWithMcpKeyAsync() {
29
29
 
30
30
  try {
31
31
  const { getCredentials } = await import('./token-store.js');
32
- const baseUrl = process.env.AINOTE_API_URL || 'https://ainote-5muq.onrender.com';
32
+ const { resolveApiUrl } = await import('../core/config-manager.js');
33
+ const baseUrl = resolveApiUrl();
33
34
  const creds = await getCredentials(baseUrl);
34
35
  if (creds && creds.mcpKey) {
35
36
  return { type: 'mcpKey', apiKey: creds.mcpKey };
@@ -82,8 +82,25 @@ function apiHost(apiUrl) {
82
82
  }
83
83
  }
84
84
 
85
+ /**
86
+ * Keychain account name for a (host, field) pair.
87
+ *
88
+ * Same cross-keychain constraint as keychainService(): account names must match
89
+ * /^[a-zA-Z0-9._@-]+$/ on every backend. The previous `${host}:${field}` form
90
+ * was rejected outright (the ':' separator, plus any ':port' in the host), so
91
+ * every keychain read/write threw and credentials always fell back to the file.
92
+ *
93
+ * Format: `${sanitizedHost}@${field}`. The host is reduced to [a-zA-Z0-9.-]
94
+ * (any other char, e.g. a port ':' or IPv6 brackets, becomes '_'), so it can
95
+ * never contain '@'; the '@' separator therefore keeps host and field
96
+ * unambiguously distinct. Deterministic for a given apiUrl.
97
+ *
98
+ * No migration: since the old ':'-based names were always rejected, nothing
99
+ * was ever stored under them in any keychain.
100
+ */
85
101
  function accountFor(apiUrl, field) {
86
- return `${apiHost(apiUrl)}:${field}`;
102
+ const host = String(apiHost(apiUrl)).replace(/[^a-zA-Z0-9.-]/g, '_');
103
+ return `${host}@${field}`;
87
104
  }
88
105
 
89
106
  function readFileStore() {
@@ -300,3 +317,10 @@ export function _credentialsLocations() {
300
317
  file: getCredentialsFile()
301
318
  };
302
319
  }
320
+
321
+ /**
322
+ * Internal helper exposing keychain account names for tests. Not public API.
323
+ */
324
+ export function _keychainAccount(apiUrl, field) {
325
+ return accountFor(apiUrl, field);
326
+ }
package/lib/cli/call.js CHANGED
@@ -10,7 +10,9 @@
10
10
  * 1. AINOTE_API_KEY env
11
11
  * 2. token store (mcpKey written by `ainote-mcp login`)
12
12
  *
13
- * Posts JSON-RPC 2.0 `tools/call` to AINOTE_API_URL (default https://api.ainote.dev/api/mcp).
13
+ * Posts JSON-RPC 2.0 `tools/call` to AINOTE_API_URL
14
+ * (default DEFAULT_API_URL from lib/core/config-manager.js, i.e. the same host
15
+ * `ainote-mcp login` stores credentials under), path /api/mcp.
14
16
  *
15
17
  * Argument parsing supports two styles, in priority order:
16
18
  * --json '{"due_today": true}' raw JSON object as arguments
@@ -26,8 +28,7 @@
26
28
  import process from 'node:process';
27
29
  import axios from 'axios';
28
30
  import { getCredentials } from '../auth/token-store.js';
29
-
30
- const DEFAULT_API_URL = 'https://api.ainote.dev';
31
+ import { DEFAULT_API_URL, resolveApiUrl } from '../core/config-manager.js';
31
32
 
32
33
  function parseArgs(argv) {
33
34
  // argv shape: [toolName, ...flags]
@@ -85,7 +86,8 @@ Usage: ainote-mcp call <tool> [options]
85
86
 
86
87
  Direct shell invocation of an ainote MCP tool over HTTP. Bypasses any
87
88
  running MCP host (Claude Desktop, Cursor, etc.) and posts directly to
88
- api.ainote.dev — useful for scripts and one-off queries.
89
+ the AI Note API (AINOTE_API_URL, default ${DEFAULT_API_URL}) — useful
90
+ for scripts and one-off queries.
89
91
 
90
92
  Authentication:
91
93
  - AINOTE_API_KEY env var, OR
@@ -135,7 +137,7 @@ export async function run(argv) {
135
137
  return;
136
138
  }
137
139
 
138
- const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
140
+ const apiUrl = resolveApiUrl();
139
141
 
140
142
  let mcpKey = process.env.AINOTE_API_KEY;
141
143
  if (!mcpKey) {
package/lib/cli/login.js CHANGED
@@ -27,8 +27,8 @@ import {
27
27
  DeviceNetworkError
28
28
  } from '../auth/device-flow.js';
29
29
  import { setCredentials, _credentialsLocations } from '../auth/token-store.js';
30
+ import { resolveApiUrl } from '../core/config-manager.js';
30
31
 
31
- const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
32
32
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
33
33
 
34
34
  function parseArgs(argv) {
@@ -117,7 +117,7 @@ async function tryOpenBrowser(uri) {
117
117
  }
118
118
 
119
119
  export async function run(argv = process.argv.slice(3)) {
120
- const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
120
+ const apiUrl = resolveApiUrl();
121
121
  const args = parseArgs(argv);
122
122
  const scopes = args.scope.split(',').map((s) => s.trim()).filter(Boolean);
123
123
 
package/lib/cli/logout.js CHANGED
@@ -4,11 +4,10 @@
4
4
  import process from 'node:process';
5
5
  import axios from 'axios';
6
6
  import { getCredentials, clearCredentials } from '../auth/token-store.js';
7
-
8
- const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
7
+ import { resolveApiUrl } from '../core/config-manager.js';
9
8
 
10
9
  export async function run() {
11
- const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
10
+ const apiUrl = resolveApiUrl();
12
11
 
13
12
  const creds = await getCredentials(apiUrl);
14
13
 
package/lib/cli/whoami.js CHANGED
@@ -12,8 +12,7 @@
12
12
  import process from 'node:process';
13
13
  import axios from 'axios';
14
14
  import { getCredentials } from '../auth/token-store.js';
15
-
16
- const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
15
+ import { resolveApiUrl } from '../core/config-manager.js';
17
16
 
18
17
  function summarizeUser(user) {
19
18
  if (!user || typeof user !== 'object') return null;
@@ -25,7 +24,7 @@ function summarizeUser(user) {
25
24
  }
26
25
 
27
26
  export async function run() {
28
- const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
27
+ const apiUrl = resolveApiUrl();
29
28
  const creds = await getCredentials(apiUrl);
30
29
 
31
30
  if (!creds) {
@@ -1,4 +1,24 @@
1
- const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
1
+ /**
2
+ * Single source of truth for the default AI Note backend URL.
3
+ *
4
+ * Every entry point (MCP server, `login`, `logout`, `whoami`, `call`) must
5
+ * resolve the API URL through resolveApiUrl(). Credentials written by
6
+ * `ainote-mcp login` are namespaced by API host in the token store, so any
7
+ * divergence here makes the other commands look up the wrong host.
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.)
11
+ */
12
+ export const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
13
+
14
+ /**
15
+ * Resolve the effective API base URL: AINOTE_API_URL env override, else the
16
+ * default. Trailing slashes are stripped so "${url}/api/..." joins cleanly.
17
+ */
18
+ export function resolveApiUrl(env = process.env) {
19
+ return (env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
20
+ }
21
+
2
22
  const DEFAULT_HTTP_PORT = 3030;
3
23
 
4
24
  const parseBoolean = (value, fallback) => {
@@ -57,7 +77,7 @@ export function getConfig() {
57
77
  return {
58
78
  api: {
59
79
  key: process.env.AINOTE_API_KEY,
60
- url: process.env.AINOTE_API_URL || DEFAULT_API_URL
80
+ url: resolveApiUrl()
61
81
  },
62
82
  http: {
63
83
  port: parseNumber(process.env.AINOTE_MCP_HTTP_PORT, DEFAULT_HTTP_PORT),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainote/mcp",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
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",