membot 0.5.2 → 0.6.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.
Files changed (40) hide show
  1. package/.claude/skills/membot.md +25 -10
  2. package/.cursor/rules/membot.mdc +25 -10
  3. package/README.md +35 -4
  4. package/package.json +8 -5
  5. package/scripts/apply-patches.sh +0 -11
  6. package/src/cli.ts +2 -2
  7. package/src/commands/login-page.mustache +50 -0
  8. package/src/commands/login.ts +83 -0
  9. package/src/config/schemas.ts +17 -5
  10. package/src/constants.ts +13 -1
  11. package/src/context.ts +1 -24
  12. package/src/db/files.ts +21 -25
  13. package/src/db/migrations/003-downloader-columns.ts +58 -0
  14. package/src/db/migrations.ts +2 -1
  15. package/src/ingest/converter/index.ts +9 -0
  16. package/src/ingest/converter/xlsx.ts +111 -0
  17. package/src/ingest/downloaders/browser.ts +180 -0
  18. package/src/ingest/downloaders/generic-web.ts +81 -0
  19. package/src/ingest/downloaders/github.ts +178 -0
  20. package/src/ingest/downloaders/google-docs.ts +56 -0
  21. package/src/ingest/downloaders/google-shared.ts +86 -0
  22. package/src/ingest/downloaders/google-sheets.ts +58 -0
  23. package/src/ingest/downloaders/google-slides.ts +53 -0
  24. package/src/ingest/downloaders/index.ts +182 -0
  25. package/src/ingest/downloaders/linear.ts +291 -0
  26. package/src/ingest/fetcher.ts +104 -129
  27. package/src/ingest/ingest.ts +43 -70
  28. package/src/mcp/instructions.ts +4 -2
  29. package/src/operations/add.ts +6 -4
  30. package/src/operations/info.ts +4 -6
  31. package/src/operations/move.ts +2 -3
  32. package/src/operations/refresh.ts +2 -4
  33. package/src/operations/remove.ts +23 -2
  34. package/src/operations/tree.ts +1 -1
  35. package/src/operations/types.ts +1 -1
  36. package/src/refresh/runner.ts +59 -114
  37. package/src/types/text-modules.d.ts +5 -0
  38. package/patches/@evantahler%2Fmcpx@0.21.4.patch +0 -51
  39. package/src/commands/mcpx.ts +0 -112
  40. package/src/ingest/agent-fetcher.ts +0 -639
@@ -1,14 +1,16 @@
1
- import type { McpxClient } from "@evantahler/mcpx";
1
+ import { join } from "node:path";
2
+ import { FILES } from "../constants.ts";
2
3
  import type { AppContext } from "../context.ts";
3
4
  import { upsertBlob } from "../db/blobs.ts";
4
5
  import { insertChunksForVersion, rebuildFts } from "../db/chunks.ts";
5
- import { getCurrent, insertVersion, millisIso, updateRefreshStatus } from "../db/files.ts";
6
+ import { type FetcherKind, getCurrent, insertVersion, millisIso, updateRefreshStatus } from "../db/files.ts";
6
7
  import { HelpfulError } from "../errors.ts";
7
8
  import { chunkDeterministic } from "../ingest/chunker.ts";
8
9
  import { convert } from "../ingest/converter/index.ts";
9
10
  import { describe } from "../ingest/describer.ts";
11
+ import { BrowserPool } from "../ingest/downloaders/browser.ts";
10
12
  import { embed } from "../ingest/embedder.ts";
11
- import { fetchRemote, isMcpToolError } from "../ingest/fetcher.ts";
13
+ import { fetchRemoteByDownloader } from "../ingest/fetcher.ts";
12
14
  import { mimeFromPath, readLocalFile, sha256Hex } from "../ingest/local-reader.ts";
13
15
  import { buildSearchText } from "../ingest/search-text.ts";
14
16
 
@@ -20,19 +22,20 @@ export interface RefreshOutcome {
20
22
  }
21
23
 
22
24
  /**
23
- * Refresh one logical_path. Re-reads its source (local stat+sha or remote
24
- * via the persisted mcpx invocation), and creates a new version only if
25
- * the source bytes changed. Always updates `refreshed_at` and
26
- * `last_refresh_status` on the row. Returns a per-path outcome — never
27
- * throws unless the path doesn't exist. The optional `onEmbedProgress`
28
- * callback is forwarded to the embedder so interactive callers (e.g. the
29
- * `refresh` operation) can drive a spinner during the slow phase.
25
+ * Refresh one logical_path. Re-reads its source (local stat+sha or
26
+ * remote via the persisted downloader name + the original URL), and
27
+ * creates a new version only if the source bytes changed. Always
28
+ * updates `refreshed_at` and `last_refresh_status` on the row. Returns
29
+ * a per-path outcome — never throws unless the path doesn't exist. The
30
+ * optional `onPhase` callback is forwarded to the embedder so
31
+ * interactive callers (e.g. the `refresh` operation) can drive a
32
+ * spinner during the slow phase.
30
33
  */
31
34
  export async function refreshOne(
32
35
  ctx: AppContext,
33
36
  logicalPath: string,
34
37
  force = false,
35
- onEmbedProgress?: (done: number, total: number) => void,
38
+ onPhase?: (sublabel: string) => void,
36
39
  ): Promise<RefreshOutcome> {
37
40
  const cur = await getCurrent(ctx.db, logicalPath);
38
41
  if (!cur) {
@@ -49,10 +52,10 @@ export async function refreshOne(
49
52
 
50
53
  try {
51
54
  if (cur.source_type === "local") {
52
- return await refreshLocal(ctx, cur, force, onEmbedProgress);
55
+ return await refreshLocal(ctx, cur, force, onPhase);
53
56
  }
54
57
  if (cur.source_type === "remote") {
55
- return await refreshRemote(ctx, cur, force, onEmbedProgress);
58
+ return await refreshRemote(ctx, cur, force, onPhase);
56
59
  }
57
60
  } catch (err) {
58
61
  const message = err instanceof Error ? err.message : String(err);
@@ -74,9 +77,8 @@ interface CurrentRow {
74
77
  source_sha256: string | null;
75
78
  mime_type: string | null;
76
79
  fetcher: string | null;
77
- fetcher_server: string | null;
78
- fetcher_tool: string | null;
79
- fetcher_args: Record<string, unknown> | null;
80
+ downloader: string | null;
81
+ downloader_args: Record<string, unknown> | null;
80
82
  refresh_frequency_sec: number | null;
81
83
  }
82
84
 
@@ -85,7 +87,7 @@ async function refreshLocal(
85
87
  ctx: AppContext,
86
88
  cur: CurrentRow,
87
89
  force: boolean,
88
- onEmbedProgress?: (done: number, total: number) => void,
90
+ onPhase?: (sublabel: string) => void,
89
91
  ): Promise<RefreshOutcome> {
90
92
  if (!cur.source_path) {
91
93
  throw new HelpfulError({
@@ -116,22 +118,28 @@ async function refreshLocal(
116
118
  sourceMtimeMs: local.mtimeMs,
117
119
  sourceSha: local.sha256,
118
120
  fetcher: "local",
119
- fetcherServer: null,
120
- fetcherTool: null,
121
- fetcherArgs: null,
121
+ downloader: null,
122
+ downloaderArgs: null,
122
123
  refreshSec: cur.refresh_frequency_sec,
123
124
  },
124
- onEmbedProgress,
125
+ onPhase,
125
126
  );
126
127
  return { logical_path: cur.logical_path, status: "ok", new_version_id: versionId };
127
128
  }
128
129
 
129
- /** Remote refresh: replay the persisted mcpx invocation, or plain HTTP. */
130
+ /**
131
+ * Remote refresh: replay the persisted downloader against the original
132
+ * URL. Each downloader is deterministic (no LLM, no agent loop), so a
133
+ * row with `downloader='google-docs'` always re-runs the Google Docs
134
+ * downloader; rows from older membot versions whose `downloader` is
135
+ * NULL fall back to URL-based dispatch (the registry's `findDownloader`
136
+ * picks the right handler from the URL itself).
137
+ */
130
138
  async function refreshRemote(
131
139
  ctx: AppContext,
132
140
  cur: CurrentRow,
133
141
  force: boolean,
134
- onEmbedProgress?: (done: number, total: number) => void,
142
+ onPhase?: (sublabel: string) => void,
135
143
  ): Promise<RefreshOutcome> {
136
144
  if (!cur.source_path) {
137
145
  throw new HelpfulError({
@@ -140,7 +148,14 @@ async function refreshRemote(
140
148
  hint: "Inspect with `membot info` and consider re-ingesting.",
141
149
  });
142
150
  }
143
- const fetched = await replayFetch(cur, ctx.mcpx);
151
+ const userDataDir = join(ctx.dataDir, FILES.BROWSER_PROFILE);
152
+ const pool = new BrowserPool({ userDataDir });
153
+ let fetched: Awaited<ReturnType<typeof fetchRemoteByDownloader>>;
154
+ try {
155
+ fetched = await fetchRemoteByDownloader(cur.downloader, cur.source_path, pool, ctx.config);
156
+ } finally {
157
+ await pool.dispose();
158
+ }
144
159
 
145
160
  if (!force && cur.source_sha256 === fetched.sha256) {
146
161
  await updateRefreshStatus(ctx.db, cur.logical_path, cur.version_id, {
@@ -161,93 +176,16 @@ async function refreshRemote(
161
176
  sourcePath: cur.source_path,
162
177
  sourceMtimeMs: null,
163
178
  sourceSha: fetched.sha256,
164
- fetcher: cur.fetcher === "mcpx" ? "mcpx" : "http",
165
- fetcherServer: fetched.fetcherServer,
166
- fetcherTool: fetched.fetcherTool,
167
- fetcherArgs: fetched.fetcherArgs,
179
+ fetcher: "downloader",
180
+ downloader: fetched.downloader,
181
+ downloaderArgs: fetched.downloaderArgs,
168
182
  refreshSec: cur.refresh_frequency_sec,
169
183
  },
170
- onEmbedProgress,
184
+ onPhase,
171
185
  );
172
186
  return { logical_path: cur.logical_path, status: "ok", new_version_id: versionId };
173
187
  }
174
188
 
175
- /**
176
- * Re-fetch a remote source. When the row recorded an mcpx invocation,
177
- * call it directly with the same args (no agent re-routing); otherwise
178
- * fall back to plain HTTP. The choice is deterministic — same row always
179
- * produces the same fetch path.
180
- */
181
- async function replayFetch(
182
- cur: CurrentRow,
183
- mcpx: McpxClient | null,
184
- ): Promise<{
185
- bytes: Uint8Array;
186
- sha256: string;
187
- mimeType: string;
188
- fetcherServer: string | null;
189
- fetcherTool: string | null;
190
- fetcherArgs: Record<string, unknown> | null;
191
- }> {
192
- if (cur.fetcher === "mcpx" && cur.fetcher_server && cur.fetcher_tool && mcpx) {
193
- const args = cur.fetcher_args ?? {};
194
- const result = await mcpx.exec(cur.fetcher_server, cur.fetcher_tool, args);
195
- if (isMcpToolError(result)) {
196
- const detail = extractText(result).trim();
197
- throw new HelpfulError({
198
- kind: "network_error",
199
- message: `mcpx tool ${cur.fetcher_server}/${cur.fetcher_tool} returned isError=true${detail ? `: ${detail}` : ""}`,
200
- hint: `Re-add with a working fetcher: \`membot remove ${cur.logical_path}\` then \`membot add ${cur.source_path} --fetcher http\` (or another --fetcher hint).`,
201
- });
202
- }
203
- const text = extractText(result);
204
- const bytes = new TextEncoder().encode(text);
205
- return {
206
- bytes,
207
- sha256: sha256Hex(bytes),
208
- mimeType: "text/markdown",
209
- fetcherServer: cur.fetcher_server,
210
- fetcherTool: cur.fetcher_tool,
211
- fetcherArgs: args,
212
- };
213
- }
214
- const r = await fetchRemote(cur.source_path ?? "", { hint: "http" });
215
- return {
216
- bytes: r.bytes,
217
- sha256: r.sha256,
218
- mimeType: r.mimeType,
219
- fetcherServer: null,
220
- fetcherTool: null,
221
- fetcherArgs: null,
222
- };
223
- }
224
-
225
- /** Pull a string out of whatever shape an mcpx tool happens to return. */
226
- function extractText(result: unknown): string {
227
- if (typeof result === "string") return result;
228
- if (result && typeof result === "object") {
229
- const r = result as Record<string, unknown>;
230
- if (typeof r.text === "string") return r.text;
231
- if (typeof r.content === "string") return r.content;
232
- if (typeof r.markdown === "string") return r.markdown;
233
- if (Array.isArray(r.content)) {
234
- const out: string[] = [];
235
- for (const c of r.content) {
236
- if (c && typeof c === "object") {
237
- const inner = c as Record<string, unknown>;
238
- if (typeof inner.text === "string") out.push(inner.text);
239
- }
240
- }
241
- if (out.length > 0) return out.join("\n\n");
242
- }
243
- }
244
- try {
245
- return JSON.stringify(result);
246
- } catch {
247
- return "";
248
- }
249
- }
250
-
251
189
  interface PipelineParams {
252
190
  logicalPath: string;
253
191
  bytes: Uint8Array;
@@ -257,10 +195,9 @@ interface PipelineParams {
257
195
  sourcePath: string | null;
258
196
  sourceMtimeMs: number | null;
259
197
  sourceSha: string;
260
- fetcher: "local" | "http" | "mcpx";
261
- fetcherServer: string | null;
262
- fetcherTool: string | null;
263
- fetcherArgs: Record<string, unknown> | null;
198
+ fetcher: FetcherKind;
199
+ downloader: string | null;
200
+ downloaderArgs: Record<string, unknown> | null;
264
201
  refreshSec: number | null;
265
202
  }
266
203
 
@@ -273,8 +210,9 @@ interface PipelineParams {
273
210
  async function runPipelineForRefresh(
274
211
  ctx: AppContext,
275
212
  p: PipelineParams,
276
- onEmbedProgress?: (done: number, total: number) => void,
213
+ onPhase?: (sublabel: string) => void,
277
214
  ): Promise<string> {
215
+ onPhase?.("storing blob");
278
216
  await upsertBlob(ctx.db, {
279
217
  sha256: p.sourceSha,
280
218
  mime_type: p.mime,
@@ -282,12 +220,17 @@ async function runPipelineForRefresh(
282
220
  bytes: p.bytes,
283
221
  });
284
222
 
223
+ onPhase?.("converting");
285
224
  const conversion = await convert(p.bytes, p.mime, p.source, ctx.config.llm);
286
225
  const markdown = conversion.markdown;
226
+ onPhase?.("describing");
287
227
  const description = await describe(p.logicalPath, p.mime, markdown, ctx.config.llm);
228
+ onPhase?.("chunking");
288
229
  const chunks = chunkDeterministic(markdown, ctx.config.chunker);
289
230
  const searchTexts = chunks.map((c) => buildSearchText(p.logicalPath, description, c.content));
290
- const embeddings = await embed(searchTexts, ctx.config.embedding_model, { onProgress: onEmbedProgress });
231
+ const embeddings = await embed(searchTexts, ctx.config.embedding_model, {
232
+ onProgress: (done, total) => onPhase?.(`embedding ${done}/${total}`),
233
+ });
291
234
 
292
235
  const versionId = millisIso(Date.now());
293
236
  const contentSha = sha256Hex(new TextEncoder().encode(markdown));
@@ -305,15 +248,16 @@ async function runPipelineForRefresh(
305
248
  mime_type: p.mime,
306
249
  size_bytes: p.bytes.byteLength,
307
250
  fetcher: p.fetcher,
308
- fetcher_server: p.fetcherServer,
309
- fetcher_tool: p.fetcherTool,
310
- fetcher_args: p.fetcherArgs,
251
+ downloader: p.downloader,
252
+ downloader_args: p.downloaderArgs,
311
253
  refresh_frequency_sec: p.refreshSec,
312
254
  refreshed_at: new Date().toISOString(),
313
255
  last_refresh_status: "ok",
314
256
  change_note: "refresh: source updated",
315
257
  });
316
258
 
259
+ onPhase?.("persisting");
260
+
317
261
  await insertChunksForVersion(
318
262
  ctx.db,
319
263
  p.logicalPath,
@@ -326,6 +270,7 @@ async function runPipelineForRefresh(
326
270
  })),
327
271
  );
328
272
 
273
+ onPhase?.("indexing");
329
274
  await rebuildFts(ctx.db);
330
275
  return versionId;
331
276
  }
@@ -7,3 +7,8 @@ declare module "*.mdc" {
7
7
  const content: string;
8
8
  export default content;
9
9
  }
10
+
11
+ declare module "*.mustache" {
12
+ const content: string;
13
+ export default content;
14
+ }
@@ -1,51 +0,0 @@
1
- diff --git a/src/search/onnx-wasm-paths.ts b/src/search/onnx-wasm-paths.ts
2
- --- a/src/search/onnx-wasm-paths.ts
3
- +++ b/src/search/onnx-wasm-paths.ts
4
- @@ -1,31 +1,16 @@
5
- -// Embed the onnxruntime-web WASM runtime files into the compiled binary
6
- -// (`bun build --compile`) so they survive in a single-binary distribution
7
- -// where the user has no node_modules.
8
- -//
9
- -// This file is loaded **dynamically** by semantic.ts. The relative paths
10
- -// only resolve in the local repo / compiled binary; for npm/bun-installed
11
- -// mcpx the parent directory layout is different (deps are hoisted), the
12
- -// dynamic import throws, and we fall back to letting transformers.js
13
- -// load WASM via its default mechanism — which works fine because in
14
- -// that environment node_modules exists and onnxruntime-web is reachable
15
- -// through normal module resolution.
16
- -
17
- -// The relative `../../node_modules/...` paths only resolve from the local repo
18
- -// layout (and inside `bun build --compile`). When this file is shipped via npm,
19
- -// deps are hoisted, so consumer `tsc` runs hit TS2307. The `ts-ignore` directive
20
- -// below silences that for consumers; we avoid the stricter `expect-error` form
21
- -// because in the local repo the path resolves fine and there would be no error
22
- -// to expect. At runtime the dynamic import in semantic.ts is wrapped in
23
- -// try/catch and falls back to transformers.js's default WASM loader (issue #85).
24
- -// biome-ignore lint/suspicious/noTsIgnore: must stay as ts-ignore per comment above
25
- -// @ts-ignore - dynamic-only import
26
- -import wasmMjsPath from "../../node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.asyncify.mjs" with {
27
- - type: "file",
28
- -};
29
- -// biome-ignore lint/suspicious/noTsIgnore: must stay as ts-ignore per comment above
30
- -// @ts-ignore - dynamic-only import
31
- -import wasmBinPath from "../../node_modules/onnxruntime-web/dist/ort-wasm-simd-threaded.asyncify.wasm" with {
32
- - type: "file",
33
- -};
34
- -
35
- -export { wasmBinPath, wasmMjsPath };
36
- +// PATCHED (membot): point mcpx's onnx-wasm-paths at the onnxruntime-web installed
37
- +// at the top of membot's node_modules. Upstream's `../../node_modules/...` only
38
- +// resolves in mcpx's standalone repo layout; for consumers we walk up 4 levels:
39
- +// node_modules/@evantahler/mcpx/src/search → node_modules → onnxruntime-web.
40
- +// biome-ignore lint/suspicious/noTsIgnore: must stay as ts-ignore — relative path only resolves at runtime in consumer layout
41
- +// @ts-ignore - dynamic-only import
42
- +import wasmMjsPath from "../../../../onnxruntime-web/dist/ort-wasm-simd-threaded.asyncify.mjs" with {
43
- + type: "file",
44
- +};
45
- +// biome-ignore lint/suspicious/noTsIgnore: must stay as ts-ignore — relative path only resolves at runtime in consumer layout
46
- +// @ts-ignore - dynamic-only import
47
- +import wasmBinPath from "../../../../onnxruntime-web/dist/ort-wasm-simd-threaded.asyncify.wasm" with {
48
- + type: "file",
49
- +};
50
- +
51
- +export { wasmBinPath, wasmMjsPath };
@@ -1,112 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { fileURLToPath } from "node:url";
3
- import type { Command } from "commander";
4
- import { logger } from "../output/logger.ts";
5
-
6
- const require = createRequire(import.meta.url);
7
-
8
- /**
9
- * Resolve the path to the bundled mcpx CLI entrypoint. We spawn it as a
10
- * child process rather than calling its functions directly so that the
11
- * upstream's argv parsing, output formatting, and config conventions stay
12
- * authoritative — `membot mcpx <subcmd>` behaves identically to the user
13
- * running `mcpx <subcmd>` themselves, just with our project's `--config`
14
- * resolution layered on top when applicable.
15
- */
16
- const MCPX_CLI = fileURLToPath(import.meta.resolve("@evantahler/mcpx/cli"));
17
-
18
- /**
19
- * Forward an argv slice to the bundled mcpx CLI. Inherits stdio so prompts
20
- * and pretty output flow through as if mcpx were called directly. Exits
21
- * the parent process with the child's status code on failure.
22
- */
23
- export async function runMcpx(args: string[]): Promise<void> {
24
- const proc = Bun.spawn(["bun", MCPX_CLI, ...args], {
25
- stdout: "inherit",
26
- stderr: "inherit",
27
- stdin: "inherit",
28
- });
29
- const code = await proc.exited;
30
- if (code !== 0) process.exit(code);
31
- }
32
-
33
- /**
34
- * Pull the verbatim argv tokens that follow `mcpx` in `process.argv`. We
35
- * forward them unmodified so flags (`--help`, `-c`, etc.) reach the
36
- * upstream CLI exactly as the user typed them.
37
- */
38
- function getRawMcpxArgs(): string[] {
39
- const idx = process.argv.indexOf("mcpx");
40
- return idx === -1 ? [] : process.argv.slice(idx + 1);
41
- }
42
-
43
- const PASSTHROUGH_SUBCOMMANDS: ReadonlyArray<[name: string, desc: string]> = [
44
- ["servers", "List configured MCP server names"],
45
- ["info", "Show server overview or schema for a specific tool"],
46
- ["search", "Search tools by keyword and/or semantic similarity"],
47
- ["exec", "Execute a tool call"],
48
- ["add", "Add an MCP server"],
49
- ["remove", "Remove an MCP server"],
50
- ["ping", "Check connectivity to MCP servers"],
51
- ["auth", "Authenticate with an HTTP MCP server"],
52
- ["deauth", "Remove stored authentication for a server"],
53
- ["resource", "List resources for a server, or read a specific resource"],
54
- ["prompt", "List prompts for a server, or get a specific prompt"],
55
- ["task", "Manage async tool tasks (list, get, result, cancel)"],
56
- ["index", "Build the search index from all configured servers"],
57
- ];
58
-
59
- /**
60
- * Register `membot mcpx <subcommand>` for every passthrough subcommand on
61
- * the upstream CLI. `--help` and unknown options are forwarded so users
62
- * always get authoritative mcpx help text.
63
- */
64
- export function registerMcpxCommand(program: Command): void {
65
- const mcpx = program.command("mcpx").description("Forward to the bundled mcpx CLI for managing MCP servers");
66
-
67
- const verifyVersion = (() => {
68
- try {
69
- const ourPkg = require("../../package.json") as { dependencies: Record<string, string> };
70
- const mcpxPkg = require("@evantahler/mcpx/package.json") as { version: string };
71
- const declared = ourPkg.dependencies["@evantahler/mcpx"];
72
- if (!declared) return true;
73
- return (
74
- mcpxPkg.version === declared ||
75
- declared.startsWith(mcpxPkg.version) ||
76
- mcpxPkg.version.startsWith(declared.replace(/^[\^~]/, ""))
77
- );
78
- } catch {
79
- return true;
80
- }
81
- })();
82
- if (!verifyVersion) {
83
- logger.warn("@evantahler/mcpx version mismatch — `membot mcpx` may behave unexpectedly.");
84
- }
85
-
86
- for (const [name, description] of PASSTHROUGH_SUBCOMMANDS) {
87
- mcpx
88
- .command(name)
89
- .description(description)
90
- .allowUnknownOption(true)
91
- .helpOption(false)
92
- .argument("[args...]", "arguments forwarded to mcpx")
93
- .action(async () => {
94
- await runMcpx(getRawMcpxArgs());
95
- });
96
- }
97
-
98
- // Upstream mcpx's "list" is the default action when invoked with no
99
- // subcommand — not a registered subcommand — so strip the "list" token
100
- // before forwarding.
101
- mcpx
102
- .command("list")
103
- .description("List all tools, resources, and prompts across all configured servers")
104
- .allowUnknownOption(true)
105
- .helpOption(false)
106
- .argument("[args...]", "arguments forwarded to mcpx")
107
- .action(async () => {
108
- const raw = getRawMcpxArgs();
109
- const args = raw[0] === "list" ? raw.slice(1) : raw;
110
- await runMcpx(args);
111
- });
112
- }