@tacuchi/agent-workflow-cli 23.0.0 → 24.0.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 (105) hide show
  1. package/README.md +37 -1
  2. package/dist/adapters/postgres-readonly-tools.js +277 -0
  3. package/dist/adapters/postgres-readonly-tools.js.map +1 -0
  4. package/dist/application/database-mcp-server.js +168 -0
  5. package/dist/application/database-mcp-server.js.map +1 -0
  6. package/dist/application/database-mcp-stdio.js +137 -0
  7. package/dist/application/database-mcp-stdio.js.map +1 -0
  8. package/dist/application/database-tool-catalog.js +226 -0
  9. package/dist/application/database-tool-catalog.js.map +1 -0
  10. package/dist/application/dsn-reader-service.js +1 -1
  11. package/dist/application/lock-service.js +96 -31
  12. package/dist/application/lock-service.js.map +1 -1
  13. package/dist/application/logging/log-events.js +19 -3
  14. package/dist/application/logging/log-events.js.map +1 -1
  15. package/dist/application/logging/logger.js +2 -2
  16. package/dist/application/logging/logger.js.map +1 -1
  17. package/dist/application/mcp-connections-service.js +115 -41
  18. package/dist/application/mcp-connections-service.js.map +1 -1
  19. package/dist/application/mcp-doctor-service.js +98 -54
  20. package/dist/application/mcp-doctor-service.js.map +1 -1
  21. package/dist/application/mcp-entry-classification.js +60 -0
  22. package/dist/application/mcp-entry-classification.js.map +1 -0
  23. package/dist/application/mcp-host-reader.js +102 -86
  24. package/dist/application/mcp-host-reader.js.map +1 -1
  25. package/dist/application/mcp-host-receipt-service.js +399 -0
  26. package/dist/application/mcp-host-receipt-service.js.map +1 -0
  27. package/dist/application/mcp-host-receipt-store.js +122 -0
  28. package/dist/application/mcp-host-receipt-store.js.map +1 -0
  29. package/dist/application/mcp-host-receipts.js +28 -0
  30. package/dist/application/mcp-host-receipts.js.map +1 -0
  31. package/dist/application/mcp-host-writer.js +209 -38
  32. package/dist/application/mcp-host-writer.js.map +1 -1
  33. package/dist/application/mcp-launch-probe-service.js +142 -0
  34. package/dist/application/mcp-launch-probe-service.js.map +1 -0
  35. package/dist/application/mcp-migration-service.js +351 -0
  36. package/dist/application/mcp-migration-service.js.map +1 -0
  37. package/dist/application/mcp-native-host-check-service.js +154 -0
  38. package/dist/application/mcp-native-host-check-service.js.map +1 -0
  39. package/dist/application/mcp-receipt-registration-service.js +105 -0
  40. package/dist/application/mcp-receipt-registration-service.js.map +1 -0
  41. package/dist/application/mcp-receipt-removal-service.js +62 -0
  42. package/dist/application/mcp-receipt-removal-service.js.map +1 -0
  43. package/dist/application/mcp-remove-service.js +83 -24
  44. package/dist/application/mcp-remove-service.js.map +1 -1
  45. package/dist/application/mcp-scope-common.js +1 -1
  46. package/dist/application/mcp-scope-common.js.map +1 -1
  47. package/dist/application/mcp-setup-service.js +59 -24
  48. package/dist/application/mcp-setup-service.js.map +1 -1
  49. package/dist/application/mcp-stdio-probe.js +526 -0
  50. package/dist/application/mcp-stdio-probe.js.map +1 -0
  51. package/dist/application/mcp-test-connection-service.js +23 -50
  52. package/dist/application/mcp-test-connection-service.js.map +1 -1
  53. package/dist/application/self/host-states.js.map +1 -1
  54. package/dist/application/self/install-skill.js +4 -34
  55. package/dist/application/self/install-skill.js.map +1 -1
  56. package/dist/application/self/mcp-config.js +369 -59
  57. package/dist/application/self/mcp-config.js.map +1 -1
  58. package/dist/application/self/mcp-via-state.js +9 -6
  59. package/dist/application/self/mcp-via-state.js.map +1 -1
  60. package/dist/application/self/uninstall-skill.js +3 -18
  61. package/dist/application/self/uninstall-skill.js.map +1 -1
  62. package/dist/application/self/uninstall.js +4 -23
  63. package/dist/application/self/uninstall.js.map +1 -1
  64. package/dist/application/workspace-init-service.js +4 -6
  65. package/dist/application/workspace-init-service.js.map +1 -1
  66. package/dist/cli/commands/index.js +2 -0
  67. package/dist/cli/commands/index.js.map +1 -1
  68. package/dist/cli/commands/mcp.js +642 -55
  69. package/dist/cli/commands/mcp.js.map +1 -1
  70. package/dist/cli/commands/tool.js +141 -0
  71. package/dist/cli/commands/tool.js.map +1 -0
  72. package/dist/cli/help-groups.js +1 -1
  73. package/dist/cli/help-groups.js.map +1 -1
  74. package/dist/cli/main.js +189 -57
  75. package/dist/cli/main.js.map +1 -1
  76. package/dist/cli/parser.js +5 -1
  77. package/dist/cli/parser.js.map +1 -1
  78. package/dist/cli/registry.js.map +1 -1
  79. package/dist/cli/render.js +15 -2
  80. package/dist/cli/render.js.map +1 -1
  81. package/dist/cli/tui/components/host-admin-section.js +2 -46
  82. package/dist/cli/tui/components/host-admin-section.js.map +1 -1
  83. package/dist/cli/tui/tabs/mcp-tab-helpers.js +71 -0
  84. package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
  85. package/dist/cli/tui/tabs/mcp-tab.js +101 -42
  86. package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
  87. package/dist/domain/database-tools.js +445 -0
  88. package/dist/domain/database-tools.js.map +1 -0
  89. package/dist/domain/flow/authority.js +4 -0
  90. package/dist/domain/flow/authority.js.map +1 -1
  91. package/dist/domain/mcp-entry.js +131 -17
  92. package/dist/domain/mcp-entry.js.map +1 -1
  93. package/dist/domain/postgres-object-search.js +320 -0
  94. package/dist/domain/postgres-object-search.js.map +1 -0
  95. package/dist/domain/redaction.js +47 -0
  96. package/dist/domain/redaction.js.map +1 -0
  97. package/dist/domain/workline-mcp-entry.js +6 -9
  98. package/dist/domain/workline-mcp-entry.js.map +1 -1
  99. package/dist/ports/postgres-tools.js +10 -0
  100. package/dist/ports/postgres-tools.js.map +1 -0
  101. package/package.json +5 -1
  102. package/dist/application/mcp-dbhub-launcher.js +0 -216
  103. package/dist/application/mcp-dbhub-launcher.js.map +0 -1
  104. package/dist/application/self/mcp-offer.js +0 -96
  105. package/dist/application/self/mcp-offer.js.map +0 -1
@@ -1,19 +1,33 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { PostgresReadonlyTools } from "../../adapters/postgres-readonly-tools.js";
6
+ import { runDatabaseMcpStdio } from "../../application/database-mcp-stdio.js";
7
+ import { DatabaseToolCatalog } from "../../application/database-tool-catalog.js";
4
8
  import { runHarness } from "../../application/dev-only-services.js";
5
9
  import { runElicitationStdio } from "../../application/elicitation-stdio.js";
6
10
  import { resolveMcpConnectionSelection, } from "../../application/mcp-connections-service.js";
7
- import { DbhubLauncherError, runDbhubLauncher } from "../../application/mcp-dbhub-launcher.js";
8
11
  import { runMcpDoctor } from "../../application/mcp-doctor-service.js";
12
+ import { classifyMcpEntry } from "../../application/mcp-entry-classification.js";
13
+ import { readMcpEntry } from "../../application/mcp-host-reader.js";
14
+ import { digestMcpReceiptDescriptor, } from "../../application/mcp-host-receipt-service.js";
15
+ import { openMcpHostReceiptService, registerPersistedMcpDescriptor, } from "../../application/mcp-host-receipts.js";
16
+ import { probePersistedMcpSetupEntries } from "../../application/mcp-launch-probe-service.js";
17
+ import { runMcpMigration, } from "../../application/mcp-migration-service.js";
18
+ import { checkNativeMcpHosts, recordNativeMcpHostChecks, } from "../../application/mcp-native-host-check-service.js";
19
+ import { registerMcpSetupReceipts } from "../../application/mcp-receipt-registration-service.js";
20
+ import { removeMcpRemoveReceipts } from "../../application/mcp-receipt-removal-service.js";
9
21
  import { runMcpRemove, } from "../../application/mcp-remove-service.js";
10
22
  import { runMcpSetup, } from "../../application/mcp-setup-service.js";
23
+ import { runMcpStdioProbe } from "../../application/mcp-stdio-probe.js";
11
24
  import { buildWarpPostInstallHint, formatWarpPostInstallHint, } from "../../application/mcp-warp-postinstall-hint.js";
12
25
  import { resolveWarpGlobalMcpPath, resolveWarpProjectMcpPath, } from "../../application/multiroot/warp.js";
13
26
  import { PathsService } from "../../application/paths-service.js";
14
27
  import { ensureWorklineMaterialized, previewWorklineMaterialization, } from "../../application/workspace-materialization-service.js";
28
+ import { DATABASE_TOOL_DESCRIPTORS } from "../../domain/database-tools.js";
15
29
  import { MCP_FILE_HOSTS, harnessById } from "../../domain/harnesses.js";
16
- import { mcpEntryNameFor } from "../../domain/mcp-entry.js";
30
+ import { buildMcpEntry, mcpEntryNameFor, } from "../../domain/mcp-entry.js";
17
31
  import { fail } from "../render.js";
18
32
  // File-writing hosts — single source in the domain (excludes oz, which has no
19
33
  // file writer). The TUI's host picker reads the same list.
@@ -21,18 +35,21 @@ const FILE_HOSTS = MCP_FILE_HOSTS;
21
35
  const HOST_VALUES = new Set([...FILE_HOSTS, "all"]);
22
36
  export const mcpCommand = {
23
37
  name: "mcp",
24
- describe: "MCP server tooling. `serve` corre el servidor propio de Workline, que presenta una frontera humana con el selector nativo del host por elicitation; su salida estándar es el canal JSON-RPC. Connections come from mcp-connections.json. Subcomandos: serve | dbhub [--instance i] | setup/remove/doctor [--host h] [--instance i|--all-connections] [--workspace dir] [--global] [--dry-run] [--force] | warp-status.",
38
+ describe: "MCP server tooling. `serve` corre el servidor de elicitation de Workline y `serve-db` sirve las tools PostgreSQL; ambas reservan stdout para JSON-RPC. `dbhub` queda como alias deprecado de serve-db. Subcomandos: serve | serve-db [--instance i] | dbhub [--instance i] | setup/remove/doctor [--host h] [--instance i|--all-connections] [--workspace dir] [--global] [--dry-run] [--force] | migrate [--host h] [--instance i|--all-connections] [--workspace dir] [--global] [--apply --force] | warp-status.",
25
39
  async execute(args, ctx) {
26
40
  const subcommand = args.rest[0];
27
41
  if ((subcommand === "setup" ||
28
42
  subcommand === "remove" ||
29
43
  subcommand === "doctor" ||
44
+ subcommand === "migrate" ||
30
45
  subcommand === "warp-status") &&
31
46
  args.rest.length > 1) {
32
47
  return fail("INVALID_INPUT", "Los subcomandos MCP no aceptan una conexión posicional. Usá --instance <nombre> o --all-connections cuando el subcomando permite fan-out.");
33
48
  }
34
49
  if (subcommand === "serve")
35
50
  return runServeSub(args);
51
+ if (subcommand === "serve-db")
52
+ return runServeDbSub(args, ctx);
36
53
  if (subcommand === "dbhub")
37
54
  return runDbhubSub(args, ctx);
38
55
  if (subcommand === "setup")
@@ -41,9 +58,11 @@ export const mcpCommand = {
41
58
  return runRemoveSub(args, ctx);
42
59
  if (subcommand === "doctor")
43
60
  return runDoctorSub(args, ctx);
61
+ if (subcommand === "migrate")
62
+ return await runMigrateSub(args, ctx);
44
63
  if (subcommand === "warp-status")
45
64
  return runWarpStatusSub(args, ctx);
46
- return fail("INVALID_INPUT", "mcp requiere subcomando: serve | dbhub [--instance <nombre>] | setup | remove | doctor | warp-status");
65
+ return fail("INVALID_INPUT", "mcp requiere subcomando: serve | serve-db [--instance <nombre>] | dbhub [--instance <nombre>] | setup | remove | doctor | migrate | warp-status");
47
66
  },
48
67
  };
49
68
  /**
@@ -71,33 +90,108 @@ async function runServeSub(args) {
71
90
  return { ok: true, exitCode: 0 };
72
91
  }
73
92
  async function runDbhubSub(args, ctx) {
93
+ process.stderr.write("aw mcp dbhub está deprecado; usá 'aw mcp serve-db'.\n");
94
+ return await runServeDbSub(args, ctx);
95
+ }
96
+ async function runServeDbSub(args, ctx) {
74
97
  if (args.rest[1] !== undefined) {
75
- return fail("INVALID_INPUT", "mcp dbhub no acepta una conexión posicional. Usá --instance <nombre>.");
98
+ return serveBootstrapFailure("mcp serve-db no acepta una conexión posicional. Usá --instance <nombre>.");
76
99
  }
100
+ if (args.flags.has("--instance"))
101
+ return serveBootstrapFailure("--instance requiere un nombre.");
77
102
  const connections = resolveConnections(args, ctx, false);
78
103
  if (!("value" in connections))
79
- return connections;
104
+ return serveBootstrapFailure("No se pudo resolver la conexión MCP.");
80
105
  const connection = connections.value[0];
81
106
  if (connection === undefined) {
82
- return fail("NO_MCP_CONNECTIONS", "No hay conexiones MCP seleccionadas para dbhub.");
107
+ return serveBootstrapFailure("No hay conexiones MCP seleccionadas para serve-db.");
83
108
  }
84
- try {
85
- const result = await runDbhubLauncher({
86
- instance: connection.name,
87
- deps: {
88
- env: { ...process.env },
89
- paths: ctx.paths,
90
- platform: process.platform,
91
- },
92
- });
93
- return { ok: true, data: undefined, exitCode: clampExit(result.exitCode) };
109
+ const catalog = new DatabaseToolCatalog({
110
+ paths: ctx.paths,
111
+ env: ctx.env,
112
+ postgres: new PostgresReadonlyTools(),
113
+ });
114
+ const onHostLoadObserved = resolveHostLoadObservation(args, ctx, connection);
115
+ await runDatabaseMcpStdio({
116
+ input: process.stdin,
117
+ output: process.stdout,
118
+ diagnostics: process.stderr,
119
+ catalog,
120
+ connection: connection.name,
121
+ ...(onHostLoadObserved === undefined ? {} : { onHostLoadObserved }),
122
+ });
123
+ return { ok: true, data: undefined, exitCode: 0 };
124
+ }
125
+ /** A stdio server may never put a CLI envelope on its JSON-RPC stdout. */
126
+ function serveBootstrapFailure(message) {
127
+ process.stderr.write(`aw mcp serve-db: ${message}\n`);
128
+ return {
129
+ ok: false,
130
+ error: { code: "MCP_SERVER_BOOTSTRAP_FAILED", message },
131
+ exitCode: 2,
132
+ suppressOutput: true,
133
+ };
134
+ }
135
+ /**
136
+ * Only an exact persisted descriptor can clear reload_required. A local doctor
137
+ * sets AW_MCP_PROBE, so its initialized notification remains launch evidence
138
+ * and cannot impersonate a real host reload.
139
+ */
140
+ function resolveHostLoadObservation(args, ctx, connection) {
141
+ if (process.env.AW_MCP_PROBE === "1")
142
+ return undefined;
143
+ const rawHost = args.values.get("host");
144
+ const rawScope = args.values.get("scope");
145
+ if (rawHost === undefined || rawScope === undefined)
146
+ return undefined;
147
+ if (!FILE_HOSTS.includes(rawHost))
148
+ return undefined;
149
+ if (rawScope !== "workspace" && rawScope !== "global")
150
+ return undefined;
151
+ const host = rawHost;
152
+ const scope = rawScope;
153
+ if (scope !== "global")
154
+ return undefined;
155
+ const entry = buildMcpEntry(connection.name, connection.dsnVar, {
156
+ host,
157
+ scope,
158
+ namespace: ctx.paths.namespace,
159
+ });
160
+ // `process.argv` begins with Node and the entrypoint. The rest must be the
161
+ // descriptor byte-for-byte; a hand-written `serve-db` command is useful but
162
+ // is not evidence that a configured host loaded its registered descriptor.
163
+ if (process.execPath !== entry.command || !isDeepStrictEqual(process.argv.slice(1), entry.args)) {
164
+ return undefined;
94
165
  }
95
- catch (err) {
96
- if (err instanceof DbhubLauncherError) {
97
- return fail("DBHUB_LAUNCHER_FAILED", err.message);
166
+ const descriptorDigest = digestMcpReceiptDescriptor({
167
+ command: entry.command,
168
+ args: entry.args,
169
+ });
170
+ return async () => {
171
+ try {
172
+ const snapshot = readMcpEntry(host, ctx.env.homeDir(), entry.name, scope);
173
+ const classification = classifyMcpEntry(host, snapshot, entry, connection);
174
+ if (classification.state !== "current" ||
175
+ snapshot.command === undefined ||
176
+ snapshot.args === undefined ||
177
+ digestMcpReceiptDescriptor({ command: snapshot.command, args: snapshot.args }) !==
178
+ descriptorDigest) {
179
+ return;
180
+ }
181
+ await openMcpHostReceiptService(ctx.paths).observeHostLoad({
182
+ host,
183
+ scope,
184
+ connection: connection.name,
185
+ descriptorDigest,
186
+ });
98
187
  }
99
- throw err;
100
- }
188
+ catch {
189
+ // A stale/missing receipt must not contaminate MCP stdout. The host has
190
+ // already received a valid initialized lifecycle response; doctor will
191
+ // surface the missing operational evidence separately.
192
+ process.stderr.write("aw mcp serve-db: no se pudo registrar la carga observada del host\n");
193
+ }
194
+ };
101
195
  }
102
196
  async function runSetupSub(args, ctx) {
103
197
  const hosts = resolveHosts(args, ctx);
@@ -116,6 +210,7 @@ async function runSetupSub(args, ctx) {
116
210
  const result = await runMcpSetupWithMaterialization(ctx, {
117
211
  hosts: hosts.value,
118
212
  connections: connections.value,
213
+ namespace: ctx.paths.namespace,
119
214
  ...scopeInput,
120
215
  dryRun: args.flags.has("--dry-run"),
121
216
  ...(args.flags.has("--force") ? { globalApproval: "explicit-cli-force" } : {}),
@@ -149,34 +244,85 @@ async function runSetupSub(args, ctx) {
149
244
  * canonical marker in place before the first host config is touched.
150
245
  */
151
246
  async function runMcpSetupWithMaterialization(ctx, input) {
152
- if (input.scope !== "workspace")
153
- return runMcpSetup(ctx.env, input);
247
+ if (input.scope !== "workspace") {
248
+ const result = runMcpSetup(ctx.env, input);
249
+ return "ok" in result ? result : await attachSetupReceipts(ctx, input, result);
250
+ }
154
251
  const preview = runMcpSetup(ctx.env, { ...input, dryRun: true });
155
252
  if ("ok" in preview)
156
253
  return preview;
157
254
  if (!hasPendingMcpMutation(preview.applied)) {
158
- return input.dryRun ? preview : runMcpSetup(ctx.env, input);
255
+ if (input.dryRun)
256
+ return preview;
257
+ const result = runMcpSetup(ctx.env, input);
258
+ return "ok" in result ? result : await attachSetupReceipts(ctx, input, result);
159
259
  }
160
260
  const materialization = await materializeMcpWorkspace(ctx, input.workspace, input.dryRun === true);
161
261
  if (input.dryRun)
162
262
  return { ...preview, materialization };
163
263
  const result = runMcpSetup(ctx.env, input);
164
- return "ok" in result ? result : { ...result, materialization };
264
+ if ("ok" in result)
265
+ return result;
266
+ const withReceipts = await attachSetupReceipts(ctx, input, result);
267
+ return { ...withReceipts, materialization };
268
+ }
269
+ async function attachSetupReceipts(ctx, input, result) {
270
+ const receipts = await registerMcpSetupReceipts(ctx.paths, input, result);
271
+ const withReceipts = {
272
+ ...result,
273
+ errors: [...result.errors, ...receipts.errors],
274
+ ...(receipts.registered.length === 0 ? {} : { receipts: receipts.registered }),
275
+ };
276
+ const launch = await probePersistedMcpSetupEntries(ctx.paths, withReceipts, receipts.probeTargets);
277
+ const native = await checkNativeMcpHosts(receipts.probeTargets);
278
+ await recordNativeMcpHostChecks({
279
+ paths: ctx.paths,
280
+ scope: withReceipts.scope,
281
+ scopeDir: withReceipts.scope_dir,
282
+ targets: receipts.probeTargets,
283
+ checks: native.checks,
284
+ });
285
+ if (receipts.registered.length === 0 &&
286
+ receipts.errors.length === 0 &&
287
+ launch.probes.length === 0 &&
288
+ launch.errors.length === 0 &&
289
+ native.checks.length === 0 &&
290
+ native.errors.length === 0) {
291
+ return result;
292
+ }
293
+ return {
294
+ ...withReceipts,
295
+ errors: [...withReceipts.errors, ...launch.errors, ...native.errors],
296
+ ...(launch.probes.length === 0 ? {} : { launch_probes: launch.probes }),
297
+ ...(native.checks.length === 0 ? {} : { native_checks: native.checks }),
298
+ };
165
299
  }
166
300
  async function runMcpRemoveWithMaterialization(ctx, input) {
167
- if (input.scope !== "workspace")
168
- return runMcpRemove(ctx.env, input);
301
+ if (input.scope !== "workspace") {
302
+ const result = runMcpRemove(ctx.env, input);
303
+ return "ok" in result ? result : await attachRemoveReceipts(ctx, input, result);
304
+ }
169
305
  const preview = runMcpRemove(ctx.env, { ...input, dryRun: true });
170
306
  if ("ok" in preview)
171
307
  return preview;
172
308
  if (!hasPendingMcpMutation(preview.removed)) {
173
- return input.dryRun ? preview : runMcpRemove(ctx.env, input);
309
+ if (input.dryRun)
310
+ return preview;
311
+ const result = runMcpRemove(ctx.env, input);
312
+ return "ok" in result ? result : await attachRemoveReceipts(ctx, input, result);
174
313
  }
175
314
  const materialization = await materializeMcpWorkspace(ctx, input.workspace, input.dryRun === true);
176
315
  if (input.dryRun)
177
316
  return { ...preview, materialization };
178
317
  const result = runMcpRemove(ctx.env, input);
179
- return "ok" in result ? result : { ...result, materialization };
318
+ if ("ok" in result)
319
+ return result;
320
+ const withReceipts = await attachRemoveReceipts(ctx, input, result);
321
+ return { ...withReceipts, materialization };
322
+ }
323
+ async function attachRemoveReceipts(ctx, input, result) {
324
+ const errors = await removeMcpRemoveReceipts(ctx.paths, input, result);
325
+ return errors.length === 0 ? result : { ...result, errors: [...result.errors, ...errors] };
180
326
  }
181
327
  function hasPendingMcpMutation(results) {
182
328
  return results.some((result) => result.action === "dry-run");
@@ -254,6 +400,7 @@ async function runRemoveSub(args, ctx) {
254
400
  const result = await runMcpRemoveWithMaterialization(ctx, {
255
401
  hosts: hosts.value,
256
402
  connections: connections.value,
403
+ namespace: ctx.paths.namespace,
257
404
  ...scopeInput,
258
405
  dryRun: args.flags.has("--dry-run"),
259
406
  ...(args.flags.has("--force") ? { globalApproval: "explicit-cli-force" } : {}),
@@ -277,38 +424,485 @@ async function runRemoveSub(args, ctx) {
277
424
  };
278
425
  }
279
426
  async function runDoctorSub(args, ctx) {
427
+ const input = resolveDoctorInput(args, ctx);
428
+ if (!("value" in input))
429
+ return input;
430
+ const data = runMcpDoctor(ctx.env, ctx.paths, {
431
+ hosts: input.value.hosts,
432
+ connections: input.value.connections,
433
+ namespace: ctx.paths.namespace,
434
+ ...input.value.scope,
435
+ });
436
+ if (input.value.probeMode !== undefined) {
437
+ await probeDoctorReports(data, input.value.probeMode, ctx, input.value.connections);
438
+ }
439
+ return doctorCommandResult(data);
440
+ }
441
+ function resolveDoctorInput(args, ctx) {
280
442
  const hosts = resolveHosts(args, ctx);
281
443
  if (!("value" in hosts))
282
444
  return hosts;
283
445
  const connections = resolveConnections(args, ctx, true);
284
446
  if (!("value" in connections))
285
447
  return connections;
448
+ const probeMode = resolveDoctorProbeMode(args);
449
+ if (!("value" in probeMode))
450
+ return probeMode;
286
451
  const workspace = args.values.get("workspace") ?? ctx.paths.workspaceDir();
287
- const scopeInput = args.flags.has("--global")
288
- ? { scope: "global" }
289
- : { scope: "workspace", workspace };
290
- const data = runMcpDoctor(ctx.env, ctx.paths, {
291
- hosts: hosts.value,
292
- connections: connections.value,
293
- ...scopeInput,
452
+ return {
453
+ value: {
454
+ hosts: hosts.value,
455
+ connections: connections.value,
456
+ probeMode: probeMode.value,
457
+ scope: args.flags.has("--global") ? { scope: "global" } : { scope: "workspace", workspace },
458
+ },
459
+ };
460
+ }
461
+ async function probeDoctorReports(data, mode, ctx, connections) {
462
+ const receipts = openMcpHostReceiptService(ctx.paths);
463
+ const catalog = new DatabaseToolCatalog({
464
+ paths: ctx.paths,
465
+ env: ctx.env,
466
+ postgres: new PostgresReadonlyTools(),
294
467
  });
295
- const okCount = data.summary.ok;
468
+ for (const report of data.reports) {
469
+ const connection = connections.find((candidate) => candidate.name === report.instance);
470
+ if (connection === undefined) {
471
+ report.probe = { mode, outcome: "failed", phase: "spawn", code: "CONNECTION_NOT_FOUND" };
472
+ continue;
473
+ }
474
+ await probeDoctorReport(report, data, mode, receipts, catalog, ctx, connection);
475
+ }
476
+ }
477
+ async function probeDoctorReport(report, data, mode, receipts, catalog, ctx, connection) {
478
+ const readiness = doctorProbeReadiness(report, mode);
479
+ if (readiness !== undefined) {
480
+ report.probe = readiness;
481
+ return;
482
+ }
483
+ const snapshot = doctorProbeSnapshot(report, data, mode, ctx, connection);
484
+ if (snapshot === undefined)
485
+ return;
486
+ const probe = await runMcpStdioProbe({
487
+ descriptor: {
488
+ command: snapshot.command,
489
+ args: snapshot.args,
490
+ ...(snapshot.env === undefined ? {} : { env: snapshot.env }),
491
+ },
492
+ mode,
493
+ expectedTools: DATABASE_TOOL_DESCRIPTORS,
494
+ });
495
+ if (!probe.ok) {
496
+ report.probe = { mode: probe.mode, outcome: "failed", phase: probe.phase, code: probe.code };
497
+ await recordReceiptProbeBestEffort(receipts, report, snapshot, "failed", probe.phase);
498
+ await inspectDoctorSafety(mode, catalog, report);
499
+ return;
500
+ }
501
+ const phase = probe.mode === "data" ? "tools/call" : "tools/list";
502
+ report.probe = { mode: probe.mode, outcome: "passed", phase };
503
+ await recordReceiptProbeBestEffort(receipts, report, snapshot, "passed", phase);
504
+ await inspectDoctorSafety(mode, catalog, report);
505
+ }
506
+ async function inspectDoctorSafety(mode, catalog, report) {
507
+ if (mode !== "data")
508
+ return;
509
+ report.safety = safetyFromRoleOutcome((await catalog.inspectRole(report.instance)).inspection);
510
+ }
511
+ function doctorProbeReadiness(report, mode) {
512
+ if (report.launch_mode === "path-dependent") {
513
+ return { mode, outcome: "failed", phase: "spawn", code: "PATH_DEPENDENT_DESCRIPTOR" };
514
+ }
515
+ if (report.status !== "ok") {
516
+ return { mode, outcome: "failed", phase: "spawn", code: "CONFIGURATION_NOT_READY" };
517
+ }
518
+ return undefined;
519
+ }
520
+ function doctorProbeSnapshot(report, data, mode, ctx, connection) {
521
+ try {
522
+ const entry = buildMcpEntry(connection.name, connection.dsnVar, {
523
+ host: report.host,
524
+ scope: data.scope,
525
+ namespace: ctx.paths.namespace,
526
+ });
527
+ const snapshot = readMcpEntry(report.host, data.scope_dir, entry.name, data.scope);
528
+ if (snapshot.command !== undefined &&
529
+ snapshot.args !== undefined &&
530
+ classifyMcpEntry(report.host, snapshot, entry, connection).state === "current") {
531
+ return {
532
+ command: snapshot.command,
533
+ args: snapshot.args,
534
+ ...(snapshot.env === undefined ? {} : { env: snapshot.env }),
535
+ };
536
+ }
537
+ }
538
+ catch {
539
+ // The status remains actionable below without exposing a host config error.
540
+ }
541
+ report.probe = { mode, outcome: "failed", phase: "spawn", code: "DESCRIPTOR_UNREADABLE" };
542
+ return undefined;
543
+ }
544
+ function doctorCommandResult(data) {
296
545
  const total = data.reports.length;
297
- const allOk = okCount === total;
546
+ const affected = data.reports.filter(isDoctorReportAffected).length;
547
+ if (affected === 0)
548
+ return { ok: true, data, exitCode: 0 };
298
549
  return {
299
- ok: allOk,
550
+ ok: false,
300
551
  data,
301
- ...(allOk
302
- ? {}
303
- : {
552
+ error: {
553
+ code: "MCP_DOCTOR_DRIFT",
554
+ message: `${affected}/${total} entradas con drift, probe o seguridad pendiente (ver data.reports)`,
555
+ },
556
+ exitCode: 1,
557
+ };
558
+ }
559
+ function isDoctorReportAffected(report) {
560
+ return (report.status !== "ok" ||
561
+ report.probe?.outcome === "failed" ||
562
+ (report.safety !== undefined && report.safety.status !== "safe"));
563
+ }
564
+ function resolveDoctorProbeMode(args) {
565
+ if (args.flags.has("--probe")) {
566
+ return fail("INVALID_INPUT", "--probe requiere launch o data.", undefined, 2);
567
+ }
568
+ const value = args.values.get("probe");
569
+ if (value === undefined)
570
+ return { value: undefined };
571
+ if (value === "launch" || value === "data")
572
+ return { value };
573
+ return fail("INVALID_INPUT", "--probe acepta launch o data.", undefined, 2);
574
+ }
575
+ async function recordReceiptProbeBestEffort(service, report, descriptor, outcome, phase) {
576
+ try {
577
+ const identity = { host: report.host, scope: report.scope, connection: report.instance };
578
+ const receipt = await service.find(identity);
579
+ const descriptorDigest = digestMcpReceiptDescriptor(descriptor);
580
+ if (receipt === undefined || receipt.descriptor_digest !== descriptorDigest) {
581
+ return;
582
+ }
583
+ await service.recordLaunchProbe({ ...identity, descriptorDigest, outcome, phase });
584
+ }
585
+ catch {
586
+ // Probe output remains useful even if an optional receipt update is busy.
587
+ // The next setup/doctor exposes reload state from the last durable receipt.
588
+ }
589
+ }
590
+ export function safetyFromRoleOutcome(inspection) {
591
+ if ("success" in inspection) {
592
+ return {
593
+ status: "blocked",
594
+ superuser: false,
595
+ write_capable: false,
596
+ create_role: false,
597
+ create_database: false,
598
+ unsafe_server_role: false,
599
+ code: inspection.code,
600
+ };
601
+ }
602
+ const status = inspection.superuser
603
+ ? "blocked"
604
+ : inspection.canWrite ||
605
+ inspection.canCreateRole ||
606
+ inspection.canCreateDatabase ||
607
+ inspection.unsafeServerRole === true
608
+ ? "warning"
609
+ : "safe";
610
+ return {
611
+ status,
612
+ superuser: inspection.superuser,
613
+ write_capable: inspection.canWrite,
614
+ create_role: inspection.canCreateRole,
615
+ create_database: inspection.canCreateDatabase,
616
+ unsafe_server_role: inspection.unsafeServerRole === true,
617
+ };
618
+ }
619
+ async function runMigrateSub(args, ctx) {
620
+ const input = resolveMigrationInput(args, ctx);
621
+ if (!("value" in input))
622
+ return input;
623
+ const migration = runMcpMigration(ctx.env, {
624
+ hosts: input.value.hosts,
625
+ connections: input.value.connections,
626
+ namespace: ctx.paths.namespace,
627
+ ...input.value.scope,
628
+ ...(input.value.apply ? { apply: true } : {}),
629
+ ...(input.value.apply && input.value.scope.scope === "global"
630
+ ? { globalApproval: true }
631
+ : {}),
632
+ });
633
+ if (isMcpMigrationRefusal(migration)) {
634
+ return fail("GLOBAL_REQUIRES_FORCE", migration.hint, undefined, 2);
635
+ }
636
+ if (input.value.apply)
637
+ await finalizeMigration(ctx, migration, input.value);
638
+ return migrationCommandResult(migration, input.value.apply);
639
+ }
640
+ function resolveMigrationInput(args, ctx) {
641
+ if (args.flags.has("--dry-run") && args.flags.has("--apply")) {
642
+ return fail("INVALID_INPUT", "mcp migrate usa preview por defecto; no combines --dry-run con --apply.", undefined, 2);
643
+ }
644
+ const apply = args.flags.has("--apply");
645
+ if (apply && !args.flags.has("--force")) {
646
+ return fail("MCP_MIGRATE_REQUIRES_FORCE", "La migración escribe configuraciones de host. Revisá el preview y repetí con --apply --force.", undefined, 2);
647
+ }
648
+ const hosts = resolveHosts(args, ctx);
649
+ if (!("value" in hosts))
650
+ return hosts;
651
+ const connections = resolveConnections(args, ctx, true);
652
+ if (!("value" in connections))
653
+ return connections;
654
+ const workspace = args.values.get("workspace") ?? ctx.paths.workspaceDir();
655
+ return {
656
+ value: {
657
+ apply,
658
+ hosts: hosts.value,
659
+ connections: connections.value,
660
+ scope: args.flags.has("--global") ? { scope: "global" } : { scope: "workspace", workspace },
661
+ },
662
+ };
663
+ }
664
+ async function finalizeMigration(ctx, data, input) {
665
+ const finalization = {
666
+ receipts: [],
667
+ receiptErrors: [],
668
+ readbackErrors: [],
669
+ nativeErrors: [],
670
+ probeTargets: [],
671
+ };
672
+ const receipts = openMcpHostReceiptService(ctx.paths);
673
+ for (const item of data.items) {
674
+ await finalizeRetiredAliasReceipts(ctx, data, input, receipts, finalization, item);
675
+ await finalizeMigrationItem(ctx, data, input, receipts, finalization, item);
676
+ }
677
+ attachMigrationFinalization(data, finalization);
678
+ if (data.scope === "global")
679
+ await finalizeMigrationProbes(ctx, data, finalization);
680
+ }
681
+ async function finalizeRetiredAliasReceipts(ctx, data, input, receipts, finalization, item) {
682
+ if (data.scope !== "global" || item.retirements === undefined)
683
+ return;
684
+ const connection = input.connections.find((candidate) => candidate.name === item.instance);
685
+ if (connection === undefined)
686
+ return;
687
+ for (const retirement of item.retirements) {
688
+ if (retirement.action !== "retire-known-legacy" || retirement.readback_state !== "missing") {
689
+ continue;
690
+ }
691
+ const aliasConnection = { ...connection, name: retirement.instance };
692
+ const entry = buildMcpEntry(aliasConnection.name, aliasConnection.dsnVar, {
693
+ host: item.host,
694
+ scope: data.scope,
695
+ namespace: ctx.paths.namespace,
696
+ });
697
+ try {
698
+ const snapshot = readMcpEntry(item.host, data.scope_dir, entry.name, data.scope);
699
+ if (classifyMcpEntry(item.host, snapshot, entry, aliasConnection).state !== "missing") {
700
+ finalization.readbackErrors.push({
701
+ host: item.host,
702
+ instance: retirement.instance,
703
+ message: "La entrada MCP legacy reapareció antes de retirar su recibo; se conserva la evidencia operativa.",
704
+ });
705
+ continue;
706
+ }
707
+ await receipts.remove({
708
+ host: item.host,
709
+ scope: data.scope,
710
+ connection: retirement.instance,
711
+ });
712
+ }
713
+ catch {
714
+ finalization.receiptErrors.push({
715
+ host: item.host,
716
+ instance: retirement.instance,
717
+ message: "La entrada MCP legacy se retiró, pero no se pudo retirar su recibo operativo.",
718
+ });
719
+ }
720
+ }
721
+ }
722
+ async function finalizeMigrationItem(ctx, data, input, receipts, finalization, item) {
723
+ const write = eligibleMigrationWrite(item);
724
+ if (write === undefined)
725
+ return;
726
+ const connection = input.connections.find((candidate) => candidate.name === item.instance);
727
+ if (connection === undefined)
728
+ return;
729
+ if (write.partial !== undefined) {
730
+ finalization.readbackErrors.push({
731
+ host: item.host,
732
+ instance: connection.name,
733
+ message: write.partial.message,
734
+ });
735
+ return;
736
+ }
737
+ if (item.readback_state !== "current") {
738
+ finalization.readbackErrors.push(migrationReadbackError(item.host, connection.name));
739
+ return;
740
+ }
741
+ if (data.scope !== "global")
742
+ return;
743
+ const current = readCurrentMigrationEntry(ctx, data, item, connection);
744
+ if (current === undefined) {
745
+ finalization.readbackErrors.push(migrationReadbackError(item.host, connection.name));
746
+ return;
747
+ }
748
+ const registration = await registerMigrationReceipt(ctx, receipts, item, connection, current.descriptor);
749
+ if (registration === "skipped")
750
+ return;
751
+ if (registration === undefined) {
752
+ finalization.receiptErrors.push({
753
+ host: item.host,
754
+ instance: connection.name,
755
+ message: "No se pudo registrar el recibo MCP; revisá el estado antes de recargar el host.",
756
+ });
757
+ return;
758
+ }
759
+ finalization.receipts.push({
760
+ host: item.host,
761
+ instance: connection.name,
762
+ descriptor_digest: registration.descriptor_digest,
763
+ reload_required: true,
764
+ });
765
+ finalization.probeTargets.push({
766
+ host: item.host,
767
+ instance: connection.name,
768
+ target: item.target,
769
+ entry: current.entry,
770
+ });
771
+ }
772
+ function eligibleMigrationWrite(item) {
773
+ // A retirement can fail only after qtc-* was written and reread. That
774
+ // partial migration still changed a live descriptor and must receive the
775
+ // same revocable receipt/reload signal as a fully retired alias; the item
776
+ // remains failed overall and the command still reports recovery work.
777
+ if (item.write?.action === "written" || item.write?.action === "skipped-idempotent") {
778
+ return item.write;
779
+ }
780
+ return undefined;
781
+ }
782
+ function readCurrentMigrationEntry(ctx, data, item, connection) {
783
+ try {
784
+ const entry = buildMcpEntry(connection.name, connection.dsnVar, {
785
+ host: item.host,
786
+ scope: data.scope,
787
+ namespace: ctx.paths.namespace,
788
+ });
789
+ const snapshot = readMcpEntry(item.host, data.scope_dir, entry.name, data.scope);
790
+ if (snapshot.command === undefined ||
791
+ snapshot.args === undefined ||
792
+ classifyMcpEntry(item.host, snapshot, entry, connection).state !== "current") {
793
+ return undefined;
794
+ }
795
+ return { entry, descriptor: { command: snapshot.command, args: snapshot.args } };
796
+ }
797
+ catch {
798
+ return undefined;
799
+ }
800
+ }
801
+ async function registerMigrationReceipt(ctx, receipts, item, connection, descriptor) {
802
+ try {
803
+ const identity = { host: item.host, scope: "global", connection: connection.name };
804
+ // A qtc descriptor can be byte-identical while this migration retired a
805
+ // legacy alias from the same live host config. That is still a mutation:
806
+ // renew the receipt so reload_required cannot remain falsely cleared from
807
+ // an earlier host load that may still expose the retired alias.
808
+ if (item.write?.action === "skipped-idempotent" && item.configuration_changed !== true) {
809
+ const existing = await receipts.find(identity);
810
+ if (existing !== undefined &&
811
+ existing.descriptor_digest === digestMcpReceiptDescriptor(descriptor)) {
812
+ return "skipped";
813
+ }
814
+ }
815
+ return await registerPersistedMcpDescriptor(ctx.paths, {
816
+ host: item.host,
817
+ scope: "global",
818
+ connection: connection.name,
819
+ entry: descriptor,
820
+ });
821
+ }
822
+ catch {
823
+ return undefined;
824
+ }
825
+ }
826
+ function migrationReadbackError(host, instance) {
827
+ return {
828
+ host,
829
+ instance,
830
+ message: "La entrada MCP escrita no coincidió al leerla de vuelta; no se registró recibo ni se debe recargar el host.",
831
+ };
832
+ }
833
+ function attachMigrationFinalization(data, state) {
834
+ if (state.receipts.length > 0)
835
+ data.receipts = state.receipts;
836
+ if (state.receiptErrors.length > 0)
837
+ data.receipt_errors = state.receiptErrors;
838
+ if (state.readbackErrors.length > 0)
839
+ data.readback_errors = state.readbackErrors;
840
+ if (state.nativeErrors.length > 0)
841
+ data.native_errors = state.nativeErrors;
842
+ }
843
+ async function finalizeMigrationProbes(ctx, data, state) {
844
+ const launch = await probePersistedMcpSetupEntries(ctx.paths, {
845
+ scope: "global",
846
+ scope_dir: data.scope_dir,
847
+ dry_run: false,
848
+ applied: [],
849
+ skipped: [],
850
+ conflicts: [],
851
+ errors: [],
852
+ }, state.probeTargets);
853
+ if (launch.probes.length > 0)
854
+ data.launch_probes = launch.probes;
855
+ if (launch.errors.length > 0) {
856
+ data.probe_errors = launch.errors.map((error) => ({
857
+ host: error.host,
858
+ instance: error.instance,
859
+ message: error.message,
860
+ }));
861
+ }
862
+ const native = await checkNativeMcpHosts(state.probeTargets);
863
+ await recordNativeMcpHostChecks({
864
+ paths: ctx.paths,
865
+ scope: "global",
866
+ scopeDir: data.scope_dir,
867
+ targets: state.probeTargets,
868
+ checks: native.checks,
869
+ });
870
+ if (native.checks.length > 0)
871
+ data.native_checks = native.checks;
872
+ state.nativeErrors.push(...native.errors.map((error) => ({
873
+ host: error.host,
874
+ instance: error.instance,
875
+ message: error.message,
876
+ })));
877
+ if (state.nativeErrors.length > 0)
878
+ data.native_errors = state.nativeErrors;
879
+ }
880
+ function migrationCommandResult(data, apply) {
881
+ const blocked = data.items.filter((item) => item.action === "blocked").length;
882
+ const failedWrites = data.items.filter((item) => item.write?.action === "conflict" || item.action === "failed").length;
883
+ const receiptFailures = data.receipt_errors?.length ?? 0;
884
+ const readbackFailures = data.readback_errors?.length ?? 0;
885
+ const probeFailures = data.probe_errors?.length ?? 0;
886
+ const nativeFailures = data.native_errors?.length ?? 0;
887
+ const problems = blocked + failedWrites + receiptFailures + readbackFailures + probeFailures + nativeFailures;
888
+ const hasProblems = apply && problems > 0;
889
+ return {
890
+ ok: !hasProblems,
891
+ data,
892
+ ...(hasProblems
893
+ ? {
304
894
  error: {
305
- code: "MCP_DOCTOR_DRIFT",
306
- message: `${total - okCount}/${total} entradas con drift (ver data.reports)`,
895
+ code: "MCP_MIGRATE_PARTIAL",
896
+ message: `${problems} entrada(s) no se migraron, no dejaron recibo, no pasaron readback, no fueron launchable o no pudieron verificarse con su host.`,
307
897
  },
308
- }),
309
- exitCode: allOk ? 0 : 1,
898
+ }
899
+ : {}),
900
+ exitCode: hasProblems ? 1 : 0,
310
901
  };
311
902
  }
903
+ function isMcpMigrationRefusal(value) {
904
+ return "ok" in value && value.ok === false;
905
+ }
312
906
  /** Exported for tests: host-harness detection must cover every file-writing host. */
313
907
  export function resolveHosts(args, ctx) {
314
908
  const flag = args.values.get("host");
@@ -346,7 +940,7 @@ function resolveConnections(args, ctx, allowAll) {
346
940
  return fail("INVALID_INPUT", "--all-connections no recibe valor.");
347
941
  }
348
942
  if (allConnections && !allowAll) {
349
- return fail("MCP_CONNECTION_SELECTION_CONFLICT", "mcp dbhub ejecuta una sola conexión; usá --instance <nombre>.");
943
+ return fail("MCP_CONNECTION_SELECTION_CONFLICT", "mcp serve-db ejecuta una sola conexión; usá --instance <nombre>.");
350
944
  }
351
945
  const instance = args.values.get("instance");
352
946
  const selection = resolveMcpConnectionSelection(ctx.paths, {
@@ -357,11 +951,4 @@ function resolveConnections(args, ctx, allowAll) {
357
951
  return fail(selection.code, selection.message);
358
952
  return { value: selection.connections };
359
953
  }
360
- function clampExit(code) {
361
- if (code === 0)
362
- return 0;
363
- if (code === 2)
364
- return 2;
365
- return 1;
366
- }
367
954
  //# sourceMappingURL=mcp.js.map