@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.
- package/README.md +37 -1
- package/dist/adapters/postgres-readonly-tools.js +277 -0
- package/dist/adapters/postgres-readonly-tools.js.map +1 -0
- package/dist/application/database-mcp-server.js +168 -0
- package/dist/application/database-mcp-server.js.map +1 -0
- package/dist/application/database-mcp-stdio.js +137 -0
- package/dist/application/database-mcp-stdio.js.map +1 -0
- package/dist/application/database-tool-catalog.js +226 -0
- package/dist/application/database-tool-catalog.js.map +1 -0
- package/dist/application/dsn-reader-service.js +1 -1
- package/dist/application/lock-service.js +96 -31
- package/dist/application/lock-service.js.map +1 -1
- package/dist/application/logging/log-events.js +19 -3
- package/dist/application/logging/log-events.js.map +1 -1
- package/dist/application/logging/logger.js +2 -2
- package/dist/application/logging/logger.js.map +1 -1
- package/dist/application/mcp-connections-service.js +115 -41
- package/dist/application/mcp-connections-service.js.map +1 -1
- package/dist/application/mcp-doctor-service.js +98 -54
- package/dist/application/mcp-doctor-service.js.map +1 -1
- package/dist/application/mcp-entry-classification.js +60 -0
- package/dist/application/mcp-entry-classification.js.map +1 -0
- package/dist/application/mcp-host-reader.js +102 -86
- package/dist/application/mcp-host-reader.js.map +1 -1
- package/dist/application/mcp-host-receipt-service.js +399 -0
- package/dist/application/mcp-host-receipt-service.js.map +1 -0
- package/dist/application/mcp-host-receipt-store.js +122 -0
- package/dist/application/mcp-host-receipt-store.js.map +1 -0
- package/dist/application/mcp-host-receipts.js +28 -0
- package/dist/application/mcp-host-receipts.js.map +1 -0
- package/dist/application/mcp-host-writer.js +209 -38
- package/dist/application/mcp-host-writer.js.map +1 -1
- package/dist/application/mcp-launch-probe-service.js +142 -0
- package/dist/application/mcp-launch-probe-service.js.map +1 -0
- package/dist/application/mcp-migration-service.js +351 -0
- package/dist/application/mcp-migration-service.js.map +1 -0
- package/dist/application/mcp-native-host-check-service.js +154 -0
- package/dist/application/mcp-native-host-check-service.js.map +1 -0
- package/dist/application/mcp-receipt-registration-service.js +105 -0
- package/dist/application/mcp-receipt-registration-service.js.map +1 -0
- package/dist/application/mcp-receipt-removal-service.js +62 -0
- package/dist/application/mcp-receipt-removal-service.js.map +1 -0
- package/dist/application/mcp-remove-service.js +83 -24
- package/dist/application/mcp-remove-service.js.map +1 -1
- package/dist/application/mcp-scope-common.js +1 -1
- package/dist/application/mcp-scope-common.js.map +1 -1
- package/dist/application/mcp-setup-service.js +59 -24
- package/dist/application/mcp-setup-service.js.map +1 -1
- package/dist/application/mcp-stdio-probe.js +526 -0
- package/dist/application/mcp-stdio-probe.js.map +1 -0
- package/dist/application/mcp-test-connection-service.js +23 -50
- package/dist/application/mcp-test-connection-service.js.map +1 -1
- package/dist/application/self/host-states.js.map +1 -1
- package/dist/application/self/install-skill.js +4 -34
- package/dist/application/self/install-skill.js.map +1 -1
- package/dist/application/self/mcp-config.js +369 -59
- package/dist/application/self/mcp-config.js.map +1 -1
- package/dist/application/self/mcp-via-state.js +9 -6
- package/dist/application/self/mcp-via-state.js.map +1 -1
- package/dist/application/self/uninstall-skill.js +3 -18
- package/dist/application/self/uninstall-skill.js.map +1 -1
- package/dist/application/self/uninstall.js +4 -23
- package/dist/application/self/uninstall.js.map +1 -1
- package/dist/application/workspace-init-service.js +4 -6
- package/dist/application/workspace-init-service.js.map +1 -1
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/mcp.js +642 -55
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/tool.js +141 -0
- package/dist/cli/commands/tool.js.map +1 -0
- package/dist/cli/help-groups.js +1 -1
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +189 -57
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/parser.js +5 -1
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/render.js +15 -2
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/tui/components/host-admin-section.js +2 -46
- package/dist/cli/tui/components/host-admin-section.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js +71 -0
- package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab.js +101 -42
- package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
- package/dist/domain/database-tools.js +445 -0
- package/dist/domain/database-tools.js.map +1 -0
- package/dist/domain/flow/authority.js +4 -0
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/mcp-entry.js +131 -17
- package/dist/domain/mcp-entry.js.map +1 -1
- package/dist/domain/postgres-object-search.js +320 -0
- package/dist/domain/postgres-object-search.js.map +1 -0
- package/dist/domain/redaction.js +47 -0
- package/dist/domain/redaction.js.map +1 -0
- package/dist/domain/workline-mcp-entry.js +6 -9
- package/dist/domain/workline-mcp-entry.js.map +1 -1
- package/dist/ports/postgres-tools.js +10 -0
- package/dist/ports/postgres-tools.js.map +1 -0
- package/package.json +5 -1
- package/dist/application/mcp-dbhub-launcher.js +0 -216
- package/dist/application/mcp-dbhub-launcher.js.map +0 -1
- package/dist/application/self/mcp-offer.js +0 -96
- package/dist/application/self/mcp-offer.js.map +0 -1
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
107
|
+
return serveBootstrapFailure("No hay conexiones MCP seleccionadas para serve-db.");
|
|
83
108
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
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
|
|
546
|
+
const affected = data.reports.filter(isDoctorReportAffected).length;
|
|
547
|
+
if (affected === 0)
|
|
548
|
+
return { ok: true, data, exitCode: 0 };
|
|
298
549
|
return {
|
|
299
|
-
ok:
|
|
550
|
+
ok: false,
|
|
300
551
|
data,
|
|
301
|
-
|
|
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: "
|
|
306
|
-
message: `${
|
|
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
|
-
|
|
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
|
|
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
|