@ainote/mcp 1.3.0 → 1.3.2

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
@@ -126,6 +126,29 @@ npx @ainote/mcp signup
126
126
 
127
127
  This will interactively ask for email, password, and name, then return your MCP API key.
128
128
 
129
+ ### CLI Tool Invocation (`call`)
130
+
131
+ Run any of the 26 ainote tools directly from the shell — no Claude / Cursor / MCP host required. Useful for scripts, alfred / raycast workflows, automation, and one-off queries.
132
+
133
+ ```bash
134
+ # List all tasks due today
135
+ npx @ainote/mcp call list_tasks --due_today=true
136
+
137
+ # Save a handoff (JSON form)
138
+ npx @ainote/mcp call handoff_save \
139
+ --json '{"project":"demo","topic":"poc","content":"..."}'
140
+
141
+ # Get a handoff
142
+ npx @ainote/mcp call handoff_get --project=demo --topic=poc
143
+
144
+ # Read a resource URI as JSON (load all tasks at once)
145
+ npx @ainote/mcp call list_tasks --limit=500
146
+ ```
147
+
148
+ Flag parsing: `--key=value` is JSON.parse-d (numbers/booleans/null/arrays/objects work directly; raw strings fall back). Pass the entire arguments object via `--json '<obj>'`. Add `--raw` for the full JSON-RPC envelope.
149
+
150
+ Auth resolution order: `AINOTE_API_KEY` env var → OS keychain (from `ainote-mcp login`).
151
+
129
152
  ### Onboarding MCP Tools
130
153
 
131
154
  These tools also work inside Claude (no API key needed):
@@ -170,7 +193,7 @@ The fallback file is only used when no system keychain is available (e.g. headle
170
193
 
171
194
  ### Environment overrides
172
195
 
173
- - `AINOTE_API_URL` — backend URL (default `https://ainote-5muq.onrender.com`)
196
+ - `AINOTE_API_URL` — backend URL (default `https://api.ainote.dev`)
174
197
  - `AINOTE_API_KEY` — pre-provisioned MCP key. When set, the login flow is bypassed entirely and this key is used as-is.
175
198
 
176
199
  ### Headless / CI usage
@@ -414,7 +437,7 @@ By default the server listens on `http://localhost:3030` and exposes two endpoin
414
437
  Environment variable | Description | Default
415
438
  ---|---|---
416
439
  `AINOTE_API_KEY` | API key used for MCP key authentication | **required**
417
- `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`
418
441
  `AINOTE_MCP_HTTP_PORT` | Local port for the SSE server | `3030`
419
442
  `AINOTE_MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed browser origins | _any_
420
443
  `AINOTE_MCP_ALLOWED_HOSTS` | Comma-separated list of allowed `Host` headers | _any_
@@ -645,7 +668,7 @@ ainote MCP works with any client that speaks Model Context Protocol over stdio o
645
668
  "mcpServers": {
646
669
  "ainote": {
647
670
  "type": "http",
648
- "url": "https://ainote-5muq.onrender.com/api/mcp",
671
+ "url": "https://api.ainote.dev/api/mcp",
649
672
  "headers": { "Authorization": "McpKey <YOUR_MCP_KEY>" }
650
673
  }
651
674
  }
package/index.js CHANGED
@@ -23,6 +23,11 @@ if (subcommand === 'signup') {
23
23
  } else if (subcommand === 'whoami') {
24
24
  const { run } = await import('./lib/cli/whoami.js');
25
25
  await run();
26
+ } else if (subcommand === 'call') {
27
+ // `ainote-mcp call <tool> [--key=value ...]` — direct shell invocation.
28
+ // Bypasses the stdio MCP host and posts JSON-RPC tools/call to the API.
29
+ const { run } = await import('./lib/cli/call.js');
30
+ await run(process.argv.slice(3));
26
31
  } else {
27
32
  // Default: start MCP stdio server
28
33
  const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
@@ -0,0 +1,211 @@
1
+ /**
2
+ * `ainote-mcp call <tool> [--key=value ...] [--json '<obj>']`
3
+ *
4
+ * Direct shell invocation of an ainote MCP tool — no agent required. Useful for:
5
+ * - one-off CLI use, scripts, alfred/raycast workflows
6
+ * - debugging tool responses without spinning up Claude
7
+ * - automation that wants a single tool result, not an interactive loop
8
+ *
9
+ * Resolves the MCP key from (in order):
10
+ * 1. AINOTE_API_KEY env
11
+ * 2. token store (mcpKey written by `ainote-mcp login`)
12
+ *
13
+ * Posts JSON-RPC 2.0 `tools/call` to AINOTE_API_URL (default https://api.ainote.dev/api/mcp).
14
+ *
15
+ * Argument parsing supports two styles, in priority order:
16
+ * --json '{"due_today": true}' raw JSON object as arguments
17
+ * --key=value one flag per top-level field; values are
18
+ * JSON.parse-d (so `--limit=5` is 5, not "5";
19
+ * `--name=hello` falls back to the raw string
20
+ * if JSON.parse fails)
21
+ *
22
+ * Output:
23
+ * default — pretty JSON of the tool's result content
24
+ * --raw — entire JSON-RPC envelope
25
+ */
26
+ import process from 'node:process';
27
+ import axios from 'axios';
28
+ import { getCredentials } from '../auth/token-store.js';
29
+
30
+ const DEFAULT_API_URL = 'https://api.ainote.dev';
31
+
32
+ function parseArgs(argv) {
33
+ // argv shape: [toolName, ...flags]
34
+ const toolName = argv[0];
35
+ const flags = argv.slice(1);
36
+ const out = { tool: toolName, args: {}, raw: false, help: false };
37
+
38
+ for (let i = 0; i < flags.length; i++) {
39
+ const a = flags[i];
40
+ if (a === '--raw') { out.raw = true; continue; }
41
+ if (a === '-h' || a === '--help') { out.help = true; continue; }
42
+ if (a === '--json') {
43
+ const next = flags[++i];
44
+ if (!next) throw new Error('--json requires a JSON object argument');
45
+ let parsed;
46
+ try { parsed = JSON.parse(next); }
47
+ catch (e) { throw new Error(`--json value is not valid JSON: ${e.message}`); }
48
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
49
+ throw new Error('--json value must be a JSON object');
50
+ }
51
+ out.args = { ...out.args, ...parsed };
52
+ continue;
53
+ }
54
+ if (a.startsWith('--')) {
55
+ const eq = a.indexOf('=');
56
+ let key, rawVal;
57
+ if (eq > 0) {
58
+ key = a.slice(2, eq);
59
+ rawVal = a.slice(eq + 1);
60
+ } else {
61
+ key = a.slice(2);
62
+ rawVal = flags[++i];
63
+ if (rawVal === undefined) {
64
+ out.args[key] = true;
65
+ continue;
66
+ }
67
+ }
68
+ // Try JSON.parse first (numbers, booleans, null, arrays/objects),
69
+ // fall back to raw string.
70
+ let value;
71
+ try { value = JSON.parse(rawVal); }
72
+ catch { value = rawVal; }
73
+ out.args[key] = value;
74
+ continue;
75
+ }
76
+ throw new Error(`unexpected positional argument: ${a}`);
77
+ }
78
+
79
+ return out;
80
+ }
81
+
82
+ function printHelp() {
83
+ const help = `\
84
+ Usage: ainote-mcp call <tool> [options]
85
+
86
+ Direct shell invocation of an ainote MCP tool over HTTP. Bypasses any
87
+ running MCP host (Claude Desktop, Cursor, etc.) and posts directly to
88
+ api.ainote.dev — useful for scripts and one-off queries.
89
+
90
+ Authentication:
91
+ - AINOTE_API_KEY env var, OR
92
+ - mcp key from \`ainote-mcp login\` (stored in OS keychain)
93
+
94
+ Options:
95
+ --key=value pass an argument to the tool (JSON.parse-d, falls
96
+ back to string). Repeatable.
97
+ --json '<obj>' pass the entire arguments object as JSON. Merged
98
+ with any --key=value flags (--key=value wins on
99
+ conflict).
100
+ --raw print the full JSON-RPC envelope instead of just
101
+ the result content.
102
+ -h, --help this help.
103
+
104
+ Examples:
105
+ ainote-mcp call list_tasks --due_today=true
106
+ ainote-mcp call list_tasks --limit=5 --status=pending
107
+ ainote-mcp call handoff_save \\
108
+ --json '{"project":"demo","topic":"poc","content":"..."}'
109
+ ainote-mcp call handoff_get --project=demo --topic=poc
110
+ ainote-mcp call list_dev_categories
111
+ `;
112
+ process.stdout.write(help);
113
+ }
114
+
115
+ export async function run(argv) {
116
+ // argv: everything after the `call` subcommand
117
+ if (!argv || argv.length === 0) {
118
+ printHelp();
119
+ process.exit(1);
120
+ return;
121
+ }
122
+
123
+ let parsed;
124
+ try { parsed = parseArgs(argv); }
125
+ catch (e) {
126
+ console.error(`error: ${e.message}\n`);
127
+ printHelp();
128
+ process.exit(2);
129
+ return;
130
+ }
131
+
132
+ if (parsed.help || !parsed.tool || parsed.tool.startsWith('-')) {
133
+ printHelp();
134
+ process.exit(parsed.help ? 0 : 1);
135
+ return;
136
+ }
137
+
138
+ const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
139
+
140
+ let mcpKey = process.env.AINOTE_API_KEY;
141
+ if (!mcpKey) {
142
+ const creds = await getCredentials(apiUrl);
143
+ mcpKey = creds?.mcpKey;
144
+ }
145
+ if (!mcpKey) {
146
+ console.error('error: no MCP key. Run `ainote-mcp login` or set AINOTE_API_KEY.');
147
+ process.exit(3);
148
+ return;
149
+ }
150
+
151
+ const body = {
152
+ jsonrpc: '2.0',
153
+ id: 1,
154
+ method: 'tools/call',
155
+ params: { name: parsed.tool, arguments: parsed.args }
156
+ };
157
+
158
+ let response;
159
+ try {
160
+ response = await axios.post(`${apiUrl}/api/mcp`, body, {
161
+ headers: {
162
+ 'Content-Type': 'application/json',
163
+ Authorization: `McpKey ${mcpKey}`,
164
+ Accept: 'application/json'
165
+ },
166
+ timeout: 60000,
167
+ validateStatus: () => true
168
+ });
169
+ } catch (e) {
170
+ console.error(`error: request failed: ${e.message}`);
171
+ process.exit(4);
172
+ return;
173
+ }
174
+
175
+ if (response.status >= 400 && response.status !== 200) {
176
+ console.error(`error: HTTP ${response.status}`);
177
+ console.error(JSON.stringify(response.data, null, 2));
178
+ process.exit(5);
179
+ return;
180
+ }
181
+
182
+ const env = response.data || {};
183
+ if (env.error) {
184
+ console.error(`error: ${env.error.message || 'unknown'} (code ${env.error.code})`);
185
+ if (env.error.data) console.error(JSON.stringify(env.error.data, null, 2));
186
+ process.exit(6);
187
+ return;
188
+ }
189
+
190
+ if (parsed.raw) {
191
+ console.log(JSON.stringify(env, null, 2));
192
+ return;
193
+ }
194
+
195
+ // Default: print just the result.content[] payload(s) so shell pipelines
196
+ // can grep / jq directly without the JSON-RPC envelope noise.
197
+ const content = env?.result?.content;
198
+ if (Array.isArray(content)) {
199
+ for (const block of content) {
200
+ if (block && typeof block === 'object' && 'text' in block) {
201
+ console.log(block.text);
202
+ } else {
203
+ console.log(JSON.stringify(block, null, 2));
204
+ }
205
+ }
206
+ return;
207
+ }
208
+
209
+ // Fallback — some tools may emit a non-standard shape; dump result.
210
+ console.log(JSON.stringify(env.result ?? env, null, 2));
211
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@ainote/mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
+ "mcpName": "io.github.seunghan91/ainote",
4
5
  "description": "MCP server for AI Note - Tasks, Dev Docs, and multi-device file sync (CLAUDE.md, memory files, Cursor rules, Windsurf rules)",
5
6
  "main": "index.js",
6
7
  "type": "module",