@aisa-one/cli 0.2.4 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] — 2026-08-18
11
+
12
+ ### Fixed
13
+
14
+ - **`aisa mcp setup` wrote a dead URL.** It hardcoded
15
+ `https://docs.aisa.one/mcp`, a hostname with no DNS record, and wrote it
16
+ into every client's config. Setup now reads the live discovery manifest at
17
+ `aisa.one/.well-known/mcp.json` and writes one entry per live server
18
+ (default set of five; `--all` for everything), so new servers work without
19
+ a CLI release. If the manifest cannot be fetched, nothing is written.
20
+ - **`{url}` entries were written for clients that cannot execute them.**
21
+ Claude Desktop and Windsurf spawn stdio processes and silently ignore url
22
+ entries; they now get an `npx mcp-remote` bridge, verified end to end
23
+ against production.
24
+ - **A config file that failed to parse was replaced with `{}`.** A
25
+ hand-edited `mcp.json` with comments or a trailing comma was wiped by
26
+ setup. It is now left untouched, with an error.
27
+
28
+ ### Added
29
+
30
+ - Entries carry the configured API key as a Bearer header; without a key
31
+ they are written credential-less and the server's OAuth challenge drives
32
+ the browser flow on first use.
33
+ - `aisa mcp status` now pings every configured endpoint with a real
34
+ initialize request instead of checking that a JSON key exists — a 401 is
35
+ reported as healthy (the auth challenge), an unresolvable hostname as the
36
+ failure it is.
37
+ - The stale dead entry earlier releases wrote is removed on the next setup.
38
+
10
39
  ## [0.2.4] — 2026-08-07
11
40
 
12
41
  Text-only follow-up to 0.2.3: no behavioural change, but the URL it printed
package/README.md CHANGED
@@ -250,15 +250,20 @@ To publish a skill, open a pull request against
250
250
  ## MCP Server
251
251
 
252
252
  ```bash
253
- aisa mcp setup # configure detected agents
254
- aisa mcp status # check configuration
253
+ aisa mcp setup # configure the default servers for every detected client
254
+ aisa mcp setup --all # every live server, not just the defaults
255
+ aisa mcp setup --agent cursor # one client only
256
+ aisa mcp status # list entries and ping each configured endpoint
255
257
  ```
256
258
 
257
- > **Not currently usable.** These commands write
258
- > `https://docs.aisa.one/mcp`, which has no DNS record, and `mcp setup`
259
- > overwrites an agent config file it cannot parse — a `mcp.json` with comments
260
- > or a trailing comma will be emptied. Avoid `aisa mcp setup` until this is
261
- > fixed.
259
+ `setup` reads the platform's discovery manifest (`aisa.one/.well-known/mcp.json`)
260
+ at run time and writes one entry per live server, in the shape each client
261
+ executes: a `url` entry for Cursor, an `npx mcp-remote` stdio bridge for
262
+ Claude Desktop and Windsurf. With an API key configured the entries carry it
263
+ as a Bearer header; without one they carry no credentials and the server's
264
+ OAuth flow opens in your browser on first use. A config file that exists but
265
+ does not parse is never overwritten. The docs-search MCP is always included
266
+ as `aisa-docs`.
262
267
 
263
268
  ## Shell Completion
264
269
 
@@ -1,4 +1,22 @@
1
+ /** One config entry in the shape the client actually executes. */
2
+ export declare function buildEntry(shape: "url" | "stdio", endpoint: string, key: string | undefined): Record<string, unknown>;
1
3
  export declare function mcpSetupAction(options: {
2
4
  agent?: string;
3
- }): void;
4
- export declare function mcpStatusAction(): void;
5
+ all?: boolean;
6
+ }): Promise<void>;
7
+ /**
8
+ * `aisa mcp status` — what is configured, and does it actually answer.
9
+ *
10
+ * The old status only checked that a config file contained an "aisa" key,
11
+ * which is exactly how a dead URL looked healthy. Reachability means a real
12
+ * HTTP exchange with each configured endpoint.
13
+ */
14
+ export declare function mcpStatusAction(): Promise<void>;
15
+ /**
16
+ * One initialize POST, no credentials. What each answer means:
17
+ * 200 -> alive and open (the docs MCP)
18
+ * 401 -> alive, auth required — the expected answer from mcp.aisa.one
19
+ * anything else -> alive but odd; say so
20
+ * network error -> the dead-hostname class this command exists to catch
21
+ */
22
+ export declare function pingEndpoint(endpoint: string): Promise<string>;
@@ -2,19 +2,78 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import chalk from "chalk";
3
3
  import { success, error, info, hint } from "../utils/display.js";
4
4
  import { expandHome, ensureDir } from "../utils/file.js";
5
- import { MCP_CONFIGS, MCP_URL } from "../constants.js";
5
+ import { MCP_CONFIGS, MCP_MANIFEST_URL, DOCS_MCP_URL, MCP_DEFAULT_SLUGS } from "../constants.js";
6
+ import { getApiKey } from "../config.js";
6
7
  import { join } from "node:path";
7
- export function mcpSetupAction(options) {
8
- const targets = options.agent && options.agent !== "all"
9
- ? [options.agent]
10
- : Object.keys(MCP_CONFIGS);
11
- let configured = 0;
8
+ const stripped = (name) => name.replace(/^AIsa\s+/i, "");
9
+ async function fetchLiveServers() {
10
+ const res = await fetch(MCP_MANIFEST_URL, { signal: AbortSignal.timeout(15_000) });
11
+ if (!res.ok)
12
+ throw new Error(`manifest answered HTTP ${res.status}`);
13
+ const manifest = (await res.json());
14
+ const live = (manifest.servers ?? [])
15
+ .filter((s) => s.status === "live" && s.slug && s.transport?.endpoint)
16
+ .map((s) => ({
17
+ slug: s.slug,
18
+ name: s.name ?? s.slug,
19
+ endpoint: s.transport.endpoint,
20
+ toolCount: s.tools?.length ?? 0,
21
+ }));
22
+ if (live.length === 0)
23
+ throw new Error("manifest lists no live servers");
24
+ return live;
25
+ }
26
+ /** One config entry in the shape the client actually executes. */
27
+ export function buildEntry(shape, endpoint, key) {
28
+ if (shape === "url") {
29
+ return key ? { url: endpoint, headers: { Authorization: `Bearer ${key}` } } : { url: endpoint };
30
+ }
31
+ // The exact invocation verified against production on 2026-08-18. The
32
+ // header value splits on the first colon, so no space after "Authorization:".
33
+ // Without a key, mcp-remote answers the server's 401 challenge by running
34
+ // the OAuth flow itself and caching tokens under ~/.mcp-auth.
35
+ const args = ["-y", "mcp-remote", endpoint];
36
+ if (key)
37
+ args.push("--header", `Authorization:Bearer ${key}`);
38
+ args.push("--transport", "http-only");
39
+ return { command: "npx", args };
40
+ }
41
+ /** The dead entry every pre-v0.3 setup wrote. Remove it on sight. */
42
+ const DEAD_URL = "https://docs.aisa.one/mcp";
43
+ export async function mcpSetupAction(options) {
44
+ const targets = options.agent && options.agent !== "all" ? [options.agent] : Object.keys(MCP_CONFIGS);
12
45
  for (const agent of targets) {
13
- const config = MCP_CONFIGS[agent];
14
- if (!config) {
46
+ if (!MCP_CONFIGS[agent]) {
15
47
  error(`Unknown agent: ${agent}. Valid: ${Object.keys(MCP_CONFIGS).join(", ")}, all`);
16
48
  return;
17
49
  }
50
+ }
51
+ // Fetch before touching any file: a setup that cannot see the manifest has
52
+ // nothing true to write.
53
+ let servers;
54
+ try {
55
+ servers = await fetchLiveServers();
56
+ }
57
+ catch (e) {
58
+ error(`Could not read the MCP manifest at ${MCP_MANIFEST_URL}: ${e.message}`);
59
+ hint("Nothing was written. Check your network and try again.");
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+ const chosen = options.all
64
+ ? servers
65
+ : servers.filter((s) => MCP_DEFAULT_SLUGS.includes(s.slug));
66
+ const skipped = servers.length - chosen.length;
67
+ const key = getApiKey();
68
+ if (key) {
69
+ info("Using your configured API key (Bearer). It will be written into each client's config file.");
70
+ }
71
+ else {
72
+ info("No API key configured — entries are written without credentials, and the");
73
+ info("server's OAuth flow will open in your browser on first use.");
74
+ }
75
+ for (const agent of targets) {
76
+ const config = MCP_CONFIGS[agent];
18
77
  const filePath = expandHome(config.path);
19
78
  let existing = {};
20
79
  if (existsSync(filePath)) {
@@ -22,46 +81,123 @@ export function mcpSetupAction(options) {
22
81
  existing = JSON.parse(readFileSync(filePath, "utf-8"));
23
82
  }
24
83
  catch {
25
- existing = {};
84
+ // A file we cannot parse is a file we do not own. The old command
85
+ // replaced it with {} — a user's hand-edited config (comments,
86
+ // trailing commas) was wiped by a "setup". Refuse instead.
87
+ error(`${agent}: ${config.path} exists but is not valid JSON — refusing to overwrite it.`);
88
+ hint("Fix or remove the file, then re-run setup.");
89
+ continue;
26
90
  }
27
91
  }
28
92
  else {
29
93
  ensureDir(join(filePath, ".."));
30
94
  }
31
- const servers = existing[config.key] || {};
32
- servers["aisa"] = { url: MCP_URL };
33
- existing[config.key] = servers;
95
+ const entries = existing[config.key] || {};
96
+ // Clean up the dead entry earlier releases wrote, and only that one:
97
+ // an "aisa" entry the user pointed somewhere else on purpose stays.
98
+ const stale = entries["aisa"];
99
+ if (stale?.url === DEAD_URL)
100
+ delete entries["aisa"];
101
+ for (const s of chosen) {
102
+ entries[`aisa-${s.slug}`] = buildEntry(config.shape, s.endpoint, key);
103
+ }
104
+ // The docs-search MCP is tiny, unauthenticated, and answers "how do I call
105
+ // this" questions — always included.
106
+ entries["aisa-docs"] = buildEntry(config.shape, DOCS_MCP_URL, undefined);
107
+ existing[config.key] = entries;
34
108
  writeFileSync(filePath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
35
- console.log(` ${chalk.green("✓")} ${agent}: ${config.path}`);
36
- configured++;
109
+ console.log(` ${chalk.green("✓")} ${agent}: ${chosen.length + 1} servers (${config.shape}) → ${config.path}`);
37
110
  }
38
- if (configured > 0) {
39
- success(`MCP server configured for ${configured} agent(s)`);
40
- hint("Restart your agent/editor to activate");
111
+ success(`Configured ${chosen.length} live servers plus docs search for ${targets.length} client(s)`);
112
+ for (const s of chosen) {
113
+ console.log(` ${chalk.cyan(`aisa-${s.slug}`)} ${stripped(s.name)} (${s.toolCount} tools)`);
41
114
  }
115
+ if (skipped > 0)
116
+ hint(`${skipped} more live servers available — run with --all to configure every one`);
117
+ hint("Restart your agent/editor to activate");
42
118
  }
43
- export function mcpStatusAction() {
44
- info(`MCP Server: ${MCP_URL}`);
119
+ /**
120
+ * `aisa mcp status` — what is configured, and does it actually answer.
121
+ *
122
+ * The old status only checked that a config file contained an "aisa" key,
123
+ * which is exactly how a dead URL looked healthy. Reachability means a real
124
+ * HTTP exchange with each configured endpoint.
125
+ */
126
+ export async function mcpStatusAction() {
127
+ const endpoints = new Map(); // endpoint -> [client/entry]
45
128
  for (const [agent, config] of Object.entries(MCP_CONFIGS)) {
46
129
  const filePath = expandHome(config.path);
47
- if (existsSync(filePath)) {
48
- try {
49
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
50
- const servers = data[config.key];
51
- if (servers?.["aisa"]) {
52
- console.log(` ${chalk.green("✓")} ${agent}: configured`);
53
- }
54
- else {
55
- console.log(` ${chalk.gray("○")} ${agent}: not configured`);
56
- }
57
- }
58
- catch {
59
- console.log(` ${chalk.gray("○")} ${agent}: config unreadable`);
60
- }
61
- }
62
- else {
130
+ if (!existsSync(filePath)) {
63
131
  console.log(` ${chalk.gray("○")} ${agent}: not installed`);
132
+ continue;
133
+ }
134
+ let entries = {};
135
+ try {
136
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
137
+ entries = data[config.key] || {};
138
+ }
139
+ catch {
140
+ console.log(` ${chalk.gray("○")} ${agent}: config unreadable`);
141
+ continue;
142
+ }
143
+ const ours = Object.entries(entries).filter(([name]) => name.startsWith("aisa"));
144
+ if (ours.length === 0) {
145
+ console.log(` ${chalk.gray("○")} ${agent}: not configured`);
146
+ continue;
64
147
  }
148
+ console.log(` ${chalk.green("✓")} ${agent}: ${ours.length} entries`);
149
+ for (const [name, entry] of ours) {
150
+ const e = entry;
151
+ const endpoint = e.url ?? e.args?.find((a) => a.startsWith("http"));
152
+ if (endpoint) {
153
+ const users = endpoints.get(endpoint) ?? [];
154
+ users.push(`${agent}/${name}`);
155
+ endpoints.set(endpoint, users);
156
+ }
157
+ }
158
+ }
159
+ if (endpoints.size === 0) {
160
+ hint("Run 'aisa mcp setup' to configure");
161
+ return;
162
+ }
163
+ console.log("");
164
+ info("Checking each configured endpoint:");
165
+ for (const [endpoint, users] of endpoints) {
166
+ console.log(` ${endpoint} ${chalk.gray(`(${users.length} entr${users.length > 1 ? "ies" : "y"})`)}`);
167
+ console.log(` ${await pingEndpoint(endpoint)}`);
168
+ }
169
+ }
170
+ /**
171
+ * One initialize POST, no credentials. What each answer means:
172
+ * 200 -> alive and open (the docs MCP)
173
+ * 401 -> alive, auth required — the expected answer from mcp.aisa.one
174
+ * anything else -> alive but odd; say so
175
+ * network error -> the dead-hostname class this command exists to catch
176
+ */
177
+ export async function pingEndpoint(endpoint) {
178
+ try {
179
+ const res = await fetch(endpoint, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
182
+ body: JSON.stringify({
183
+ jsonrpc: "2.0",
184
+ id: 1,
185
+ method: "initialize",
186
+ params: {
187
+ protocolVersion: "2025-06-18",
188
+ capabilities: {},
189
+ clientInfo: { name: "aisa-cli", version: "status" },
190
+ },
191
+ }),
192
+ signal: AbortSignal.timeout(8_000),
193
+ });
194
+ if (res.status === 200)
195
+ return `${chalk.green("✓")} reachable (open)`;
196
+ if (res.status === 401)
197
+ return `${chalk.green("✓")} reachable (auth required — normal)`;
198
+ return `${chalk.yellow("⚠")} reachable but answered HTTP ${res.status}`;
199
+ }
200
+ catch (e) {
201
+ return `${chalk.red("✗")} unreachable: ${e.message}`;
65
202
  }
66
- hint("Run 'aisa mcp setup' to configure");
67
203
  }
@@ -1,8 +1,27 @@
1
- export declare const VERSION = "0.2.4";
1
+ export declare const VERSION = "0.3.0";
2
2
  /** Root of the platform. Per-surface bases are derived in api.ts#resolveBases. */
3
3
  export declare const BASE_URL = "https://api.aisa.one";
4
4
  export declare const ENV_VAR_NAME = "AISA_API_KEY";
5
- export declare const MCP_URL = "https://docs.aisa.one/mcp";
5
+ /**
6
+ * The MCP discovery manifest — the single source of truth for which servers
7
+ * exist and where they live. The CLI deliberately knows no server list of its
8
+ * own: servers ship and retire on the platform side, and reading the manifest
9
+ * at setup time means a new server works without a CLI release (the same
10
+ * philosophy as LLM_ROUTE_PREFIXES below, applied to the fast-moving side).
11
+ *
12
+ * History: until v0.2.4 this file hardcoded MCP_URL = "https://docs.aisa.one/mcp",
13
+ * a host that does not resolve — every `aisa mcp setup` wrote a dead entry into
14
+ * users' configs, and nothing noticed because the command had no tests.
15
+ */
16
+ export declare const MCP_MANIFEST_URL = "https://aisa.one/.well-known/mcp.json";
17
+ /** Mintlify's docs-search MCP (3 tools: search/read/feedback). Unauthenticated. */
18
+ export declare const DOCS_MCP_URL = "https://aisa.one/docs/mcp";
19
+ /**
20
+ * Servers configured by default. Eleven entries per client is a lot of config;
21
+ * these five cover the broadest agent needs, and `--all` configures everything
22
+ * the manifest lists as live.
23
+ */
24
+ export declare const MCP_DEFAULT_SLUGS: readonly string[];
6
25
  /**
7
26
  * First path segment of every route the LLM gateway serves under /v1 (plus
8
27
  * /v1beta for Gemini). Everything else is an integration API under /apis/v1.
@@ -15,9 +34,17 @@ export declare const MCP_URL = "https://docs.aisa.one/mcp";
15
34
  */
16
35
  export declare const LLM_ROUTE_PREFIXES: readonly string[];
17
36
  export declare const AGENT_DIRS: Record<string, string>;
37
+ /**
38
+ * Per-client config shape. This is the part v0.2.4 got wrong even apart from
39
+ * the dead URL: it wrote `{url}` everywhere, but Claude Desktop's mcpServers
40
+ * only executes command/args (stdio) entries and silently ignores url ones.
41
+ * Stdio clients get an `npx mcp-remote` bridge instead — verified end to end
42
+ * against mcp.aisa.one (initialize -> tools/list -> tools/call) on 2026-08-18.
43
+ */
18
44
  export declare const MCP_CONFIGS: Record<string, {
19
45
  path: string;
20
46
  key: string;
47
+ shape: "url" | "stdio";
21
48
  }>;
22
49
  /**
23
50
  * Ticker symbol → CoinGecko coin id.
package/dist/constants.js CHANGED
@@ -1,8 +1,33 @@
1
- export const VERSION = "0.2.4";
1
+ export const VERSION = "0.3.0";
2
2
  /** Root of the platform. Per-surface bases are derived in api.ts#resolveBases. */
3
3
  export const BASE_URL = "https://api.aisa.one";
4
4
  export const ENV_VAR_NAME = "AISA_API_KEY";
5
- export const MCP_URL = "https://docs.aisa.one/mcp";
5
+ /**
6
+ * The MCP discovery manifest — the single source of truth for which servers
7
+ * exist and where they live. The CLI deliberately knows no server list of its
8
+ * own: servers ship and retire on the platform side, and reading the manifest
9
+ * at setup time means a new server works without a CLI release (the same
10
+ * philosophy as LLM_ROUTE_PREFIXES below, applied to the fast-moving side).
11
+ *
12
+ * History: until v0.2.4 this file hardcoded MCP_URL = "https://docs.aisa.one/mcp",
13
+ * a host that does not resolve — every `aisa mcp setup` wrote a dead entry into
14
+ * users' configs, and nothing noticed because the command had no tests.
15
+ */
16
+ export const MCP_MANIFEST_URL = "https://aisa.one/.well-known/mcp.json";
17
+ /** Mintlify's docs-search MCP (3 tools: search/read/feedback). Unauthenticated. */
18
+ export const DOCS_MCP_URL = "https://aisa.one/docs/mcp";
19
+ /**
20
+ * Servers configured by default. Eleven entries per client is a lot of config;
21
+ * these five cover the broadest agent needs, and `--all` configures everything
22
+ * the manifest lists as live.
23
+ */
24
+ export const MCP_DEFAULT_SLUGS = [
25
+ "web-search",
26
+ "twitter-api",
27
+ "crypto-market-data",
28
+ "marketpulse",
29
+ "stock-pulse",
30
+ ];
6
31
  /**
7
32
  * First path segment of every route the LLM gateway serves under /v1 (plus
8
33
  * /v1beta for Gemini). Everything else is an integration API under /apis/v1.
@@ -35,15 +60,24 @@ export const AGENT_DIRS = {
35
60
  gemini: "~/.gemini/skills/",
36
61
  openclaw: "~/.openclaw/skills/",
37
62
  };
63
+ /**
64
+ * Per-client config shape. This is the part v0.2.4 got wrong even apart from
65
+ * the dead URL: it wrote `{url}` everywhere, but Claude Desktop's mcpServers
66
+ * only executes command/args (stdio) entries and silently ignores url ones.
67
+ * Stdio clients get an `npx mcp-remote` bridge instead — verified end to end
68
+ * against mcp.aisa.one (initialize -> tools/list -> tools/call) on 2026-08-18.
69
+ */
38
70
  export const MCP_CONFIGS = {
39
- cursor: { path: "~/.cursor/mcp.json", key: "mcpServers" },
71
+ cursor: { path: "~/.cursor/mcp.json", key: "mcpServers", shape: "url" },
40
72
  "claude-desktop": {
41
73
  path: "~/Library/Application Support/Claude/claude_desktop_config.json",
42
74
  key: "mcpServers",
75
+ shape: "stdio",
43
76
  },
44
77
  windsurf: {
45
78
  path: "~/.codeium/windsurf/mcp_config.json",
46
79
  key: "mcpServers",
80
+ shape: "stdio",
47
81
  },
48
82
  };
49
83
  /**
package/dist/index.js CHANGED
@@ -472,12 +472,13 @@ skills
472
472
  const mcp = program.command("mcp").description("MCP server integration");
473
473
  mcp
474
474
  .command("setup")
475
- .description("Configure AIsa MCP server for AI agents")
475
+ .description("Configure AIsa MCP servers (from the live manifest) for AI agents")
476
476
  .option("--agent <agent>", "Target agent: cursor, claude-desktop, windsurf, all")
477
+ .option("--all", "Configure every live server, not just the default set")
477
478
  .action(mcpSetupAction);
478
479
  mcp
479
480
  .command("status")
480
- .description("Check MCP server configuration status")
481
+ .description("Check MCP configuration and ping each configured endpoint")
481
482
  .action(mcpStatusAction);
482
483
  // ── Config ──
483
484
  const configCmd = program.command("config").description("Manage CLI configuration");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aisa-one/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for the AIsa unified AI infrastructure platform - one API key for 80+ LLMs and 900+ endpoints across finance, search, social, and video APIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",