@agentprojectcontext/apx 1.53.7 → 1.55.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.53.7",
3
+ "version": "1.55.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -153,7 +153,9 @@ class McpProcess {
153
153
 
154
154
  async listTools() {
155
155
  await this._ensureInitialized();
156
- return this._send("tools/list", {});
156
+ return collectToolPages((cursor) =>
157
+ this._send("tools/list", cursor ? { cursor } : {})
158
+ );
157
159
  }
158
160
 
159
161
  async callTool(name, args) {
@@ -374,7 +376,9 @@ class HttpMcpClient {
374
376
 
375
377
  async listTools() {
376
378
  await this._ensureInitialized();
377
- return this._rpc("tools/list", {});
379
+ return collectToolPages((cursor) =>
380
+ this._rpc("tools/list", cursor ? { cursor } : {})
381
+ );
378
382
  }
379
383
 
380
384
  async callTool(name, args) {
@@ -399,6 +403,21 @@ class HttpMcpClient {
399
403
  }
400
404
  }
401
405
 
406
+ // tools/list is paginated (nextCursor). Follow every page and hand back a
407
+ // single merged { tools } result so callers never see partial catalogs.
408
+ const MAX_TOOL_PAGES = 32;
409
+ async function collectToolPages(fetchPage) {
410
+ const tools = [];
411
+ let cursor;
412
+ for (let i = 0; i < MAX_TOOL_PAGES; i++) {
413
+ const result = await fetchPage(cursor);
414
+ if (Array.isArray(result?.tools)) tools.push(...result.tools);
415
+ cursor = result?.nextCursor;
416
+ if (!cursor) break;
417
+ }
418
+ return { tools };
419
+ }
420
+
402
421
  function parseFirstSseJson(raw) {
403
422
  for (const block of raw.split(/\r?\n\r?\n/)) {
404
423
  const dataLines = [];
@@ -21,7 +21,7 @@ If you can spawn a subagent natively in the current IDE (Claude Code, Cursor,
21
21
  |-------|-----------|------|
22
22
  | Delegate to external coding CLI | **apx-runtime** | `apx run <agent> --runtime claude-code\|codex\|...` |
23
23
  | List/read/resume/summarise/continue sessions | **apx-sessions** | `apx session resume`, `apx sessions list`, "import a codex session" |
24
- | Use a registered MCP tool | **apx-mcp** | `apx mcp run`, "call MCP filesystem", "MCP failing" |
24
+ | Use a registered MCP tool | **apx-mcp** | `apx mcp tools`, `apx mcp run`, "call MCP filesystem", "MCP failing" |
25
25
  | Add/configure/use a project agent | **apx-agent** | "add an agent", vault import, per-agent model, agent memory |
26
26
  | Register/list/configure a project | **apx-project** | "register this project", `apx project list`, per-project config |
27
27
  | Per-project TODO list | **apx-task** | "add a task", "remind me to…", "what's pending" |
@@ -47,7 +47,12 @@ apx mcp remove github --scope runtime --project iacrmar
47
47
  apx mcp enable filesystem --project iacrmar
48
48
  apx mcp disable filesystem --project iacrmar
49
49
 
50
- # Call a tool through the daemon (debugging)
50
+ # Discover tools list catalog, then inspect one tool's schema
51
+ apx mcp tools filesystem # table: tool name + description
52
+ apx mcp tools filesystem read_file # params (types, required) + run example
53
+ apx mcp tools filesystem --json # raw JSON with full inputSchema
54
+
55
+ # Call a tool through the daemon
51
56
  apx mcp run filesystem read_file '{"path":"README.md"}'
52
57
  ```
53
58
 
@@ -95,13 +100,15 @@ apx mcp remove github # errors if github lives in runtime
95
100
 
96
101
  ```bash
97
102
  apx mcp check --project iacrmar # scopes seen + which files exist
98
- apx mcp run <name> <tool> '{...}' # spawn server, call a tool
103
+ apx mcp tools <name> # spawn server + list its tools (proves init works)
104
+ apx mcp logs <name> # spawn/init event log + stderr tail
105
+ apx mcp run <name> <tool> '{...}' # call a tool for real
99
106
  apx log -f # tail unified log for spawn errors
100
107
  ```
101
108
 
102
- "Doesn't show tools" = command failed to start (missing env vars, package not found) or crashed during initialize. Unified log holds the stderr buffer.
109
+ "Doesn't show tools" = command failed to start (missing env vars, package not found) or crashed during initialize. `apx mcp logs <name>` shows the stderr tail; the unified log has the rest.
103
110
 
104
- > `apx mcp tools <name>` is a placeholder stub ("coming in v0.2"). Use `apx mcp run` to verify spawn.
111
+ Standard workflow to use any MCP: `apx mcp tools <name>` `apx mcp tools <name> <tool>` (copy the run example) `apx mcp run <name> <tool> '<json>'`.
105
112
 
106
113
  ## Don't
107
114
 
@@ -151,10 +151,17 @@ apx mcp add github \
151
151
  ## Debugging
152
152
 
153
153
  ```bash
154
- # Smoke test (apx mcp tools is a v0.2 stub don't rely on it)
154
+ # Smoke test spawn the server and list its tool catalog
155
+ apx mcp tools my-server
156
+
157
+ # Inspect one tool's schema + copy-paste run example
158
+ apx mcp tools my-server search_inventory
159
+
160
+ # Call it for real
155
161
  apx mcp run my-server search_inventory '{"query":"shoes"}'
156
162
 
157
- # Spawn errors / stderr
163
+ # Spawn errors / stderr tail
164
+ apx mcp logs my-server
158
165
  apx log -f
159
166
 
160
167
  # Scopes / files / env APX sees
@@ -591,6 +591,75 @@ export function readGlobalMessages({ channel, limit = 100, since } = {}) {
591
591
  return all.slice(-limit);
592
592
  }
593
593
 
594
+ // ---------------------------------------------------------------------------
595
+ // Global channel threads (super-agent chats surfaced in the web Chats sidebar)
596
+ // ---------------------------------------------------------------------------
597
+ // The global ledger is the source of truth for every super-agent turn outside
598
+ // exec (telegram, web quick-chat, desktop, deck …). A "thread" is one
599
+ // channel+day JSONL file — the same granularity the context window reads.
600
+
601
+ const CHANNEL_NAME_RE = /^[a-z0-9_-]+$/i;
602
+
603
+ // List every non-empty channel+day thread, newest-last-activity first.
604
+ export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
605
+ const base = _globalMessagesDir || GLOBAL_MESSAGES_DIR;
606
+ if (!fs.existsSync(base)) return [];
607
+ const chans = (channels && channels.length
608
+ ? channels
609
+ : fs.readdirSync(base).filter((f) => {
610
+ try { return fs.statSync(path.join(base, f)).isDirectory(); } catch { return false; }
611
+ })
612
+ ).filter((c) => CHANNEL_NAME_RE.test(c));
613
+
614
+ const out = [];
615
+ for (const ch of chans) {
616
+ const dir = path.join(base, ch);
617
+ let files;
618
+ try { files = fs.readdirSync(dir); } catch { continue; }
619
+ for (const f of files) {
620
+ const m = f.match(/^(\d{4}-\d{2}-\d{2})\.jsonl$/);
621
+ if (!m) continue;
622
+ const msgs = parseDayJsonl(fs.readFileSync(path.join(dir, f), "utf8")).filter(
623
+ (r) => r.type === "user" || r.type === "agent"
624
+ );
625
+ if (!msgs.length) continue;
626
+ const firstUser = msgs.find((r) => r.type === "user");
627
+ const title = String((firstUser || msgs[0]).body || "")
628
+ .replace(/\s+/g, " ")
629
+ .trim()
630
+ .slice(0, 80);
631
+ out.push({
632
+ id: m[1],
633
+ channel: ch,
634
+ title: title || `${ch} · ${m[1]}`,
635
+ messages: msgs.length,
636
+ started_at: msgs[0].ts,
637
+ last_ts: msgs[msgs.length - 1].ts,
638
+ });
639
+ }
640
+ }
641
+ out.sort((a, b) => (b.last_ts || "").localeCompare(a.last_ts || ""));
642
+ return out;
643
+ }
644
+
645
+ // Read one channel+day thread shaped for the web chat viewer:
646
+ // { id, channel, messages: [{ role, content, ts }] } — or null when missing.
647
+ export function readGlobalThread({ channel, date, _globalMessagesDir } = {}) {
648
+ if (!CHANNEL_NAME_RE.test(String(channel || ""))) return null;
649
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ""))) return null;
650
+ const base = _globalMessagesDir || GLOBAL_MESSAGES_DIR;
651
+ const file = path.join(base, channel, `${date}.jsonl`);
652
+ if (!fs.existsSync(file)) return null;
653
+ const messages = parseDayJsonl(fs.readFileSync(file, "utf8"))
654
+ .filter((r) => r.type === "user" || r.type === "agent")
655
+ .map((r) => ({
656
+ role: r.type === "user" ? "user" : "assistant",
657
+ content: r.body || "",
658
+ ts: r.ts,
659
+ }));
660
+ return { id: date, channel, messages };
661
+ }
662
+
594
663
  // Wipe the cache and re-populate from APX project messages. Reads BOTH `.jsonl`
595
664
  // (current format) and `.md` (legacy). Called by rebuild.
596
665
  export function rebuildMessagesFromFs(db, projectRoot) {
@@ -1,11 +1,14 @@
1
1
  // Per-agent conversation surface: list, fetch, compact, and a2a /send.
2
2
  // GET /projects/:pid/agents/:slug/conversations
3
3
  // GET /projects/:pid/agents/:slug/conversations/:id
4
+ // GET /projects/:pid/super-agent/threads (channel ledger)
5
+ // GET /projects/:pid/super-agent/threads/:channel/:id
4
6
  // POST /projects/:pid/agents/:slug/compact
5
7
  // POST /projects/:pid/agents/:slug/conversations/:id/compact
6
8
  // POST /projects/:pid/send (agent-to-agent)
7
9
  import { readAgents } from "#core/apc/parser.js";
8
10
  import { listConversations, readConversation } from "#core/stores/conversations.js";
11
+ import { listGlobalThreads, readGlobalThread } from "#core/stores/messages.js";
9
12
  import { compactConversation } from "#core/stores/conversations-compactor.js";
10
13
  import { replyAsAgent } from "#core/agent/a2a/reply.js";
11
14
  import { nowIso } from "./shared.js";
@@ -48,6 +51,28 @@ export function register(app, { project, config }) {
48
51
  });
49
52
  });
50
53
 
54
+ // ---- Super-agent channel threads ----
55
+ // The super-agent's chats (telegram, web quick-chat, desktop, deck …) live
56
+ // in the global per-channel ledger, not in per-agent conversation files.
57
+ // These endpoints surface that ledger as day-threads so the web Chats
58
+ // sidebar can list and reopen them.
59
+ app.get("/projects/:pid/super-agent/threads", (req, res) => {
60
+ const p = project(req, res);
61
+ if (!p) return;
62
+ res.json(listGlobalThreads());
63
+ });
64
+
65
+ app.get("/projects/:pid/super-agent/threads/:channel/:id", (req, res) => {
66
+ const p = project(req, res);
67
+ if (!p) return;
68
+ const thread = readGlobalThread({
69
+ channel: req.params.channel,
70
+ date: req.params.id,
71
+ });
72
+ if (!thread) return res.status(404).json({ error: "thread not found" });
73
+ res.json(thread);
74
+ });
75
+
51
76
  async function handleCompact(req, res, filename) {
52
77
  const p = project(req, res);
53
78
  if (!p) return;
@@ -5,6 +5,7 @@
5
5
  // POST /projects/:pid/mcps?scope=shared|runtime|global (default: shared)
6
6
  // DELETE /projects/:pid/mcps/:name?scope=… (default: shared)
7
7
  // GET /projects/:pid/mcps/check
8
+ // GET /projects/:pid/mcps/:name/tools
8
9
  // POST /projects/:pid/mcps/:name/call
9
10
  import fs from "node:fs";
10
11
  import path from "node:path";
@@ -190,6 +191,20 @@ export function register(app, { projects, registries, project }) {
190
191
  });
191
192
  });
192
193
 
194
+ // Full tool catalog — tools/list with input schemas, all pages merged.
195
+ // This is what `apx mcp tools` renders; /test below stays as the
196
+ // lightweight smoke check for the web UI card.
197
+ app.get("/projects/:pid/mcps/:name/tools", async (req, res) => {
198
+ const p = project(req, res);
199
+ if (!p) return;
200
+ try {
201
+ const result = await registries.for(p).listTools(req.params.name);
202
+ res.json({ tools: Array.isArray(result?.tools) ? result.tools : [] });
203
+ } catch (e) {
204
+ res.status(500).json({ error: e.message });
205
+ }
206
+ });
207
+
193
208
  app.post("/projects/:pid/mcps/:name/call", async (req, res) => {
194
209
  const p = project(req, res);
195
210
  if (!p) return;
@@ -0,0 +1,56 @@
1
+ // `apx code` — launch the APX terminal coding assistant (Solid.js TUI).
2
+ // The TUI runs its TypeScript source directly under bun; there is no
3
+ // legacy readline fallback anymore (removed with the old sys.js chat).
4
+ import { existsSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, resolve } from "node:path";
7
+ import { spawnSync } from "node:child_process";
8
+ import { resolveProjectId } from "./project.js";
9
+ import { readConfig } from "#core/config/index.js";
10
+ import { readIdentity } from "#core/identity/index.js";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const TUI_SRC = resolve(__dirname, "../../tui/run.ts");
14
+
15
+ export async function cmdCode(args) {
16
+ const pid = await resolveProjectId(args?.flags?.project);
17
+ const cfg = readConfig();
18
+ const id = readIdentity();
19
+
20
+ // Optional --agent <slug>: route chat to a project agent instead of the APX
21
+ // default. Empty / missing flag means default ("super-agent" mode).
22
+ const agentFlag = typeof args?.flags?.agent === "string" ? args.flags.agent.trim() : "";
23
+ const routedAgentSlug = agentFlag || null;
24
+ const defaultAgentLabel = id?.agent_name || cfg.super_agent?.name || "APX";
25
+
26
+ if (!existsSync(TUI_SRC)) {
27
+ throw new Error(
28
+ "apx code: TUI source not found at src/interfaces/tui/run.ts — reinstall with `npm i -g @agentprojectcontext/apx`."
29
+ );
30
+ }
31
+
32
+ // bun must resolve node_modules/tsconfig from the apx package root, so the
33
+ // spawn cwd stays there — but we pass the user's actual working directory
34
+ // (where they ran `apx code`) via --cwd so the TUI shows the real project
35
+ // path + git branch instead of apx/src.
36
+ const bunBin = process.env.BUN_PATH || "bun";
37
+ const userCwd = process.cwd();
38
+ const result = spawnSync(bunBin, [
39
+ "--preload", "@opentui/solid/preload",
40
+ TUI_SRC,
41
+ "--pid", pid,
42
+ "--agent", routedAgentSlug || defaultAgentLabel,
43
+ "--model", cfg.super_agent?.model || "claude-3-5-sonnet",
44
+ "--cwd", userCwd,
45
+ ], { stdio: "inherit", cwd: resolve(__dirname, "../../..") });
46
+
47
+ if (result.error?.code === "ENOENT") {
48
+ throw new Error(
49
+ "apx code: bun is required to run the TUI but was not found. Install it (https://bun.sh) or set BUN_PATH to the binary."
50
+ );
51
+ }
52
+ if (result.error) throw result.error;
53
+ if (typeof result.status === "number" && result.status !== 0) {
54
+ process.exitCode = result.status;
55
+ }
56
+ }
@@ -159,13 +159,130 @@ export async function cmdMcpRun(args) {
159
159
  process.stdout.write(JSON.stringify(result.result, null, 2) + "\n");
160
160
  }
161
161
 
162
+ // Turn a JSON-Schema type into a short placeholder for the run-example JSON.
163
+ function placeholderFor(schema) {
164
+ const t = Array.isArray(schema?.type) ? schema.type[0] : schema?.type;
165
+ if (schema?.enum?.length) return schema.enum[0];
166
+ if (t === "number" || t === "integer") return 0;
167
+ if (t === "boolean") return false;
168
+ if (t === "array") return [];
169
+ if (t === "object") return {};
170
+ return `<${t || "string"}>`;
171
+ }
172
+
173
+ function schemaTypeLabel(schema) {
174
+ if (schema?.enum?.length) return schema.enum.join("|");
175
+ const t = Array.isArray(schema?.type) ? schema.type.join("|") : schema?.type;
176
+ return t || "any";
177
+ }
178
+
179
+ function firstLine(s) {
180
+ return String(s || "").split("\n")[0].trim();
181
+ }
182
+
183
+ function printToolDetail(mcpName, tool) {
184
+ console.log(`${mcpName} · ${tool.name}`);
185
+ if (tool.description) console.log(` ${tool.description.trim().replace(/\n/g, "\n ")}`);
186
+
187
+ const props = tool.inputSchema?.properties || {};
188
+ const required = new Set(tool.inputSchema?.required || []);
189
+ const keys = Object.keys(props);
190
+ console.log("");
191
+ if (keys.length === 0) {
192
+ console.log(" Params: (none)");
193
+ } else {
194
+ console.log(" Params:");
195
+ const nameW = Math.max(...keys.map((k) => k.length), 4) + 2;
196
+ const typeW = Math.max(...keys.map((k) => schemaTypeLabel(props[k]).length), 4) + 2;
197
+ for (const k of keys) {
198
+ const req = required.has(k) ? "(required)" : "(optional)";
199
+ console.log(
200
+ ` ${k.padEnd(nameW)}${schemaTypeLabel(props[k]).padEnd(typeW)}${req} ${firstLine(props[k]?.description)}`
201
+ );
202
+ }
203
+ }
204
+
205
+ // Example invocation with the required params stubbed in.
206
+ const example = {};
207
+ for (const k of keys) {
208
+ if (required.has(k)) example[k] = placeholderFor(props[k]);
209
+ }
210
+ console.log("");
211
+ console.log(" Run:");
212
+ console.log(` apx mcp run ${mcpName} ${tool.name} '${JSON.stringify(example)}'`);
213
+ }
214
+
162
215
  export async function cmdMcpTools(args) {
163
216
  const name = args._[0];
164
- if (!name) throw new Error("apx mcp tools: missing <name>");
165
- // Daemon doesn't have a dedicated tools/list endpoint yet; we'd extend it in v0.2.
166
- // For now, print a hint:
167
- console.log(`(apx mcp tools list of tools/list will arrive in v0.2)`);
168
- console.log(`To call a tool: apx mcp run ${name} <tool> '<json>'`);
217
+ if (!name) throw new Error("apx mcp tools: usage: apx mcp tools <name> [<tool>] [--json]");
218
+ const toolFilter = args._[1];
219
+ const pid = await resolveProjectId(args?.flags?.project);
220
+ const data = await http.get(`/projects/${pid}/mcps/${name}/tools`);
221
+ const tools = data.tools || [];
222
+
223
+ if (toolFilter) {
224
+ const tool = tools.find((t) => t.name === toolFilter);
225
+ if (!tool) {
226
+ const hint = tools.length
227
+ ? `Available: ${tools.map((t) => t.name).join(", ")}`
228
+ : "(server reported no tools)";
229
+ throw new Error(`MCP "${name}" has no tool "${toolFilter}". ${hint}`);
230
+ }
231
+ if (args?.flags?.json) {
232
+ process.stdout.write(JSON.stringify(tool, null, 2) + "\n");
233
+ return;
234
+ }
235
+ printToolDetail(name, tool);
236
+ return;
237
+ }
238
+
239
+ if (args?.flags?.json) {
240
+ process.stdout.write(JSON.stringify(tools, null, 2) + "\n");
241
+ return;
242
+ }
243
+ if (tools.length === 0) {
244
+ console.log(`(MCP "${name}" reported no tools)`);
245
+ return;
246
+ }
247
+ const nameW = Math.max(...tools.map((t) => t.name.length), 4) + 2;
248
+ console.log(`${tools.length} tool${tools.length === 1 ? "" : "s"} — apx mcp tools ${name} <tool> for schema\n`);
249
+ console.log("TOOL".padEnd(nameW) + " DESCRIPTION");
250
+ for (const t of tools) {
251
+ console.log(t.name.padEnd(nameW) + " " + firstLine(t.description).slice(0, 100));
252
+ }
253
+ }
254
+
255
+ export async function cmdMcpLogs(args) {
256
+ const name = args._[0];
257
+ if (!name) throw new Error("apx mcp logs: missing <name>");
258
+ const pid = await resolveProjectId(args?.flags?.project);
259
+ const logs = await http.get(`/projects/${pid}/mcps/${name}/logs`);
260
+ if (args?.flags?.json) {
261
+ process.stdout.write(JSON.stringify(logs, null, 2) + "\n");
262
+ return;
263
+ }
264
+ const target = logs.transport === "http"
265
+ ? logs.url
266
+ : [logs.command, ...(logs.args || [])].filter(Boolean).join(" ");
267
+ console.log(`${name} (${logs.transport})${target ? " — " + target : ""}`);
268
+ if (logs.transport === "stdio") {
269
+ console.log(` running: ${logs.running ? "yes" : "no"} started: ${logs.started_at || "-"} last exit: ${logs.last_exit_code ?? "-"}`);
270
+ } else {
271
+ console.log(` started: ${logs.started_at || "-"} last error: ${logs.last_error || "-"}`);
272
+ }
273
+ if (logs.note) console.log(` ${logs.note}`);
274
+ if (logs.events?.length) {
275
+ console.log("\nEvents:");
276
+ for (const e of logs.events) {
277
+ console.log(` ${e.ts} [${e.level}] ${e.msg}`);
278
+ }
279
+ }
280
+ if (logs.stderr_tail?.trim()) {
281
+ console.log("\nstderr tail:");
282
+ for (const line of logs.stderr_tail.trim().split("\n")) {
283
+ console.log(` ${line}`);
284
+ }
285
+ }
169
286
  }
170
287
 
171
288
  export async function cmdMcpCheck(args = {}) {
@@ -46,6 +46,7 @@ import {
46
46
  cmdMcpDisable,
47
47
  cmdMcpRun,
48
48
  cmdMcpTools,
49
+ cmdMcpLogs,
49
50
  cmdMcpCheck,
50
51
  } from "./commands/mcp.js";
51
52
  import {
@@ -89,7 +90,7 @@ import {
89
90
  cmdConversationsList,
90
91
  cmdConversationsGet,
91
92
  } from "./commands/chat.js";
92
- import { cmdSys as cmdCode } from "./commands/sys.js";
93
+ import { cmdCode } from "./commands/code.js";
93
94
  import { cmdRun, cmdEnvDetect } from "./commands/runtime.js";
94
95
  import { cmdSend, cmdConnections } from "./commands/a2a.js";
95
96
  import {
@@ -713,7 +714,8 @@ const HELP_TOPICS = new Map(Object.entries({
713
714
  ["enable <name>", "Enable a project-owned MCP server."],
714
715
  ["disable <name>", "Disable a project-owned MCP server."],
715
716
  ["run <name> <tool>", "Call one MCP tool."],
716
- ["tools <name>", "Show tool-list hint."],
717
+ ["tools <name> [<tool>]", "List a server's tools, or show one tool's schema."],
718
+ ["logs <name>", "Show spawn/init logs and stderr tail for a server."],
717
719
  ["check", "Audit source files, merge order, and conflicts."],
718
720
  ],
719
721
  options: [["--project <name|id|path>", "Pin command to a specific project."]],
@@ -785,10 +787,27 @@ const HELP_TOPICS = new Map(Object.entries({
785
787
  }),
786
788
  "mcp tools": topic({
787
789
  title: "apx mcp tools",
788
- summary: "Show MCP tool-list guidance for a server.",
789
- usage: ["apx mcp tools <name> [--project <name|id|path>]"],
790
- options: [["--project <name|id|path>", "Pin command to a specific project."]],
791
- examples: ["apx mcp tools filesystem"],
790
+ summary: "List an MCP server's tools, or show one tool's input schema with a ready-to-run example.",
791
+ usage: ["apx mcp tools <name> [<tool>] [--json] [--project <name|id|path>]"],
792
+ options: [
793
+ ["--json", "Raw JSON output (full tool objects with inputSchema)."],
794
+ ["--project <name|id|path>", "Pin command to a specific project."],
795
+ ],
796
+ examples: [
797
+ "apx mcp tools filesystem",
798
+ "apx mcp tools filesystem read_file",
799
+ "apx mcp tools dokploy-mcp --json",
800
+ ],
801
+ }),
802
+ "mcp logs": topic({
803
+ title: "apx mcp logs",
804
+ summary: "Show an MCP server's spawn/init event log and stderr tail — first stop when a server doesn't list tools.",
805
+ usage: ["apx mcp logs <name> [--json] [--project <name|id|path>]"],
806
+ options: [
807
+ ["--json", "Raw JSON output."],
808
+ ["--project <name|id|path>", "Pin command to a specific project."],
809
+ ],
810
+ examples: ["apx mcp logs dokploy-mcp"],
792
811
  }),
793
812
  "mcp check": topic({
794
813
  title: "apx mcp check",
@@ -2022,7 +2041,8 @@ function buildHelp(version) {
2022
2041
  hCmd("apx mcp remove <name>", 36, ""),
2023
2042
  hCmd("apx mcp enable/disable", 36, "<name>"),
2024
2043
  hCmd("apx mcp run <name> <tool>", 36, "[<json-args>] call a tool through the daemon"),
2025
- hCmd("apx mcp tools <name>", 36, "list available tools"),
2044
+ hCmd("apx mcp tools <name>", 36, "[<tool>] list tools, or one tool's schema + run example"),
2045
+ hCmd("apx mcp logs <name>", 36, "spawn/init log + stderr tail"),
2026
2046
  hCmd("apx mcp check", 36, "audit multi-source merge"),
2027
2047
 
2028
2048
  hSec("Daemon Service"),
@@ -2372,6 +2392,7 @@ async function dispatch(cmd, rest) {
2372
2392
  else if (sub === "disable") await cmdMcpDisable(a);
2373
2393
  else if (sub === "run") await cmdMcpRun(a);
2374
2394
  else if (sub === "tools") await cmdMcpTools(a);
2395
+ else if (sub === "logs") await cmdMcpLogs(a);
2375
2396
  else if (sub === "check") await cmdMcpCheck(a);
2376
2397
  else die(`unknown mcp subcommand: ${sub || "(none)"}`);
2377
2398
  break;