@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
package/README.md CHANGED
@@ -124,10 +124,46 @@ Running `agent-workflow` (or `aw`) with no arguments opens the tab-based TUI:
124
124
  | **Status** | Doctor dashboard: CLI / hosts / hooks / MCP tiles + daily operational logs. The hosts tile jumps to [Workline]. |
125
125
  | **Workline** | Per-host administration of the bundled `w` SKILL (install / reinstall / uninstall, `hooks armed` state) plus a compact flows overview. |
126
126
  | **Project** | Workspace sources, branches and git-flow actions. |
127
- | **MCP** | dbhub connections. `mcp-connections.json` is the only authority for connection names and exact DSN variables: register with `aw self mcp use-env --name alpha --dsn-var ALPHA_DATABASE_URL`. Direct MCP operations select the sole connection, require `--instance <name>` when several exist, or use an explicit `--all-connections` fan-out. **Install writes the host's user-scope config** (e.g. `~/.claude.json`, `~/.codex/config.toml`) — never the project `.mcp.json`; `aw mcp setup` remains the workspace-capable path (workspace by default; `--workspace <dir>` / `--global --force`). |
127
+ | **MCP** | Tools PostgreSQL de solo lectura. `mcp-connections.json` v2 es la única autoridad de alias, provider y variable DSN: registrá con `aw self mcp use-env --name alpha --dsn-var ALPHA_DATABASE_URL`. La TUI instala descriptores absolutos en user scope; `aw mcp setup` conserva además el modo workspace portable dependiente de `PATH`. Cada host muestra configuración, launchability, recarga pendiente y carga observada; Codex queda opcional porque existe el fallback local `aw tool call`. |
128
128
  | **Skills** | Standalone third-party skills manager (skills.sh model): register from `owner/repo`, a git URL (`#ref` supported) or an absolute local path; install materializes a canonical copy in `~/.agents/skills/<name>` (the open-standard dir every non-Claude host scans) plus a symlink replica in `~/.claude/skills/<name>` (copy fallback where symlinks are unavailable). Seeded with the recommended external skills from the companion marketplace README — keep both lists in sync. |
129
129
  | **Config** | Namespace, host-targeting preferences, and the workspace branch defaults (written to the WORKSPACE block). |
130
130
 
131
+ ### PostgreSQL MCP y fallback local
132
+
133
+ V1 publica sólo `execute_sql` y `search_objects` para PostgreSQL. Ambas rutas usan el
134
+ mismo catálogo y el mismo JSON canónico: el servidor MCP devuelve ese JSON en
135
+ `content[0].text`, y el CLI lo imprime directamente, sin el envelope general.
136
+
137
+ ```sh
138
+ aw tool list --connection qtc-cert
139
+ aw tool call execute_sql --connection qtc-cert --input-json '{"sql":"SELECT 1"}'
140
+ printf '%s' '{"object_type":"table","pattern":"user%"}' \
141
+ | aw tool call search_objects --connection qtc-cert --input-json -
142
+ ```
143
+
144
+ `execute_sql` acepta una sola sentencia de lectura y ejecuta cada llamada en una conexión
145
+ nueva, transacción `READ ONLY`, timeout de 30 s y rollback final. El resultado se limita a
146
+ 1.000 filas y 4 MiB; la entrada JSON se limita a 1 MiB. Nunca pongas un DSN, SQL ni resultados
147
+ en los archivos de host, recibos u logs operativos.
148
+
149
+ `search_objects` conserva el envelope de DBHub 1.2.1: `object_type`, `pattern`, filtros
150
+ opcionales, `detail_level`, `count`, `results` y `truncated`. Sus siete tipos y tres niveles
151
+ mantienen los campos de descubrimiento de DBHub; `truncated` sólo vale `true` cuando se observó
152
+ una fila adicional que quedó fuera del límite.
153
+
154
+ `READ ONLY` no reemplaza una cuenta PostgreSQL de mínimo privilegio: no uses superuser, permisos
155
+ de escritura/creación, ni membresía o vía `SET ROLE` hacia `pg_signal_backend`,
156
+ `pg_signal_autovacuum_worker`, `pg_read_server_files`, `pg_write_server_files` o
157
+ `pg_execute_server_program`. El ejecutor rechaza superuser y esos roles de servidor; con
158
+ `aw mcp doctor --probe data`, los demás privilegios de escritura se informan como advertencia y la
159
+ conexión no queda marcada como segura. Tampoco concedas `EXECUTE` sobre extensiones o funciones con
160
+ efectos externos.
161
+
162
+ `aw mcp serve-db --instance <nombre>` es el servidor stdio; `aw mcp dbhub` sigue durante una
163
+ versión como alias deprecado. `aw mcp doctor --probe launch` comprueba `initialize → initialized
164
+ → tools/list`; `--probe data` añade `SELECT 1 AS ok`. `aw mcp migrate` sólo muestra preview;
165
+ escribir requiere `--apply --force` y no se ejecuta automáticamente.
166
+
131
167
  ## Namespace resolution
132
168
 
133
169
  Workspace artifacts live under `.<namespace>/`. Resolution order (first match wins):
@@ -0,0 +1,277 @@
1
+ import { Client } from "pg";
2
+ import Cursor from "pg-cursor";
3
+ import { MAX_TOOL_RESULT_BYTES, MAX_TOOL_ROWS } from "../domain/database-tools.js";
4
+ import { PostgresToolError } from "../ports/postgres-tools.js";
5
+ const CONNECTION_TIMEOUT_MS = 10_000;
6
+ const STATEMENT_TIMEOUT_MS = 30_000;
7
+ /**
8
+ * The PostgreSQL adapter owns the safety envelope for every catalog call:
9
+ * connection lifetime, transaction read-only state, timeout, bounded rows and
10
+ * disposal. It never returns a driver error or DSN to the caller.
11
+ */
12
+ export class PostgresReadonlyTools {
13
+ async execute(sql, dsn, options = {}) {
14
+ return await this.withReadonlyClient(dsn, async (client) => await readBoundedCursor(client, sql, [], options.signal), ...(options.signal === undefined ? [] : [{ signal: options.signal }]));
15
+ }
16
+ async query(sql, values, dsn, options = {}) {
17
+ return await this.withReadonlyClient(dsn, async (client) => await readBoundedCursor(client, sql, values, options.signal), ...(options.signal === undefined ? [] : [{ signal: options.signal }]));
18
+ }
19
+ async inspectRole(dsn) {
20
+ return await this.withReadonlyClient(dsn, async (client) => {
21
+ const result = await client.query(`SELECT
22
+ r.rolsuper AS superuser,
23
+ r.rolcreaterole AS can_create_role,
24
+ r.rolcreatedb AS can_create_database,
25
+ (
26
+ has_database_privilege(current_database(), 'CREATE')
27
+ OR EXISTS (
28
+ SELECT 1 FROM pg_namespace n
29
+ WHERE has_schema_privilege(n.oid, 'CREATE')
30
+ )
31
+ OR EXISTS (
32
+ SELECT 1 FROM pg_class c
33
+ WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f')
34
+ AND (
35
+ has_table_privilege(c.oid, 'INSERT, UPDATE, DELETE, TRUNCATE')
36
+ OR has_any_column_privilege(c.oid, 'INSERT, UPDATE')
37
+ )
38
+ )
39
+ OR EXISTS (
40
+ SELECT 1 FROM pg_class sequence_class
41
+ WHERE sequence_class.relkind = 'S'
42
+ AND has_sequence_privilege(sequence_class.oid, 'UPDATE')
43
+ )
44
+ ) AS can_write,
45
+ COALESCE(
46
+ (
47
+ SELECT bool_or(
48
+ pg_has_role(current_user, dangerous_role.oid, 'USAGE')
49
+ OR pg_has_role(current_user, dangerous_role.oid, 'MEMBER')
50
+ )
51
+ FROM pg_roles dangerous_role
52
+ WHERE dangerous_role.rolname IN (
53
+ 'pg_signal_backend',
54
+ 'pg_signal_autovacuum_worker',
55
+ 'pg_read_server_files',
56
+ 'pg_write_server_files',
57
+ 'pg_execute_server_program'
58
+ )
59
+ ),
60
+ false
61
+ ) AS unsafe_server_role
62
+ FROM pg_roles r
63
+ WHERE r.rolname = current_user`);
64
+ const role = result.rows[0];
65
+ if (role === undefined) {
66
+ throw new PostgresToolError("DATABASE_ROLE_UNKNOWN", "No se pudo verificar el rol PostgreSQL.");
67
+ }
68
+ return {
69
+ superuser: role.superuser,
70
+ canCreateRole: role.can_create_role,
71
+ canCreateDatabase: role.can_create_database,
72
+ canWrite: role.can_write,
73
+ unsafeServerRole: role.unsafe_server_role,
74
+ transactionReadOnly: true,
75
+ };
76
+ }, { allowSuperuser: true });
77
+ }
78
+ async withReadonlyClient(dsn, work, options = {}) {
79
+ const client = new Client({
80
+ connectionString: dsn,
81
+ connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
82
+ statement_timeout: STATEMENT_TIMEOUT_MS,
83
+ });
84
+ // A connected pg client can emit a later socket error after rejecting the
85
+ // active query. Keep it from becoming an unhandled EventEmitter error; the
86
+ // awaited operation below still maps it to a secret-free tool failure.
87
+ client.on("error", () => { });
88
+ let transactionStarted = false;
89
+ let connected = false;
90
+ const abort = () => {
91
+ // Calls use an isolated client, so destroying it is a reliable PostgreSQL
92
+ // cancellation mechanism. It avoids `pg`'s `Client.cancel` trap, whose
93
+ // receiver must be a separate control client and can otherwise corrupt
94
+ // the active connection. The server rolls the read-only transaction back.
95
+ void client.end().catch(() => undefined);
96
+ };
97
+ if (options.signal?.aborted)
98
+ throw queryCancelled();
99
+ options.signal?.addEventListener("abort", abort, { once: true });
100
+ try {
101
+ await client.connect();
102
+ connected = true;
103
+ await client.query("SET default_transaction_read_only = on");
104
+ await client.query("BEGIN READ ONLY");
105
+ transactionStarted = true;
106
+ const readOnly = await client.query("SHOW transaction_read_only");
107
+ if (readOnly.rows[0]?.transaction_read_only !== "on") {
108
+ throw new PostgresToolError("READ_ONLY_POLICY", "PostgreSQL no confirmó una transacción de solo lectura.");
109
+ }
110
+ if (!options.allowSuperuser)
111
+ await rejectUnsafeExecutionRole(client);
112
+ await client.query("SET LOCAL statement_timeout = '30s'");
113
+ return await work(client);
114
+ }
115
+ catch (error) {
116
+ if (options.signal?.aborted)
117
+ throw queryCancelled();
118
+ if (error instanceof PostgresToolError)
119
+ throw error;
120
+ throw safeDriverError(error, connected);
121
+ }
122
+ finally {
123
+ options.signal?.removeEventListener("abort", abort);
124
+ // An abort tears down this per-call socket, which makes PostgreSQL roll
125
+ // the transaction back itself. Do not issue a query or wait for cursor
126
+ // cleanup on a connection that intentionally cannot become ready again.
127
+ if (transactionStarted && !options.signal?.aborted) {
128
+ await client.query("ROLLBACK").catch(() => undefined);
129
+ }
130
+ await client.end().catch(() => undefined);
131
+ }
132
+ }
133
+ }
134
+ async function readBoundedCursor(client, sql, values, signal) {
135
+ if (signal?.aborted)
136
+ throw queryCancelled();
137
+ const cursor = client.query(new Cursor(sql, [...values]));
138
+ const cancel = () => {
139
+ // `withReadonlyClient` also observes this signal during connection/setup.
140
+ // Keep the cursor listener so a live read is terminated immediately.
141
+ void client.end().catch(() => undefined);
142
+ };
143
+ signal?.addEventListener("abort", cancel, { once: true });
144
+ let closeCursor = true;
145
+ try {
146
+ return await readCursorRows(cursor, signal);
147
+ }
148
+ catch (error) {
149
+ closeCursor = closesActiveCursorAfter(error);
150
+ if (signal?.aborted)
151
+ throw queryCancelled();
152
+ throw error;
153
+ }
154
+ finally {
155
+ signal?.removeEventListener("abort", cancel);
156
+ // pg-cursor close waits for ReadyForQuery. After explicit cancellation the
157
+ // isolated client socket was destroyed, so waiting here would turn a
158
+ // successful cancel into a hung finally; the client teardown owns cleanup.
159
+ // pg-cursor sends ReadyForQuery itself after a query error. Calling close
160
+ // after its read promise rejects can subscribe too late and wait forever
161
+ // for a second ReadyForQuery. The isolated transaction rollback below owns
162
+ // cleanup in that case; close only when the portal still needs a Close +
163
+ // Sync (normal completion or a local size-bound rejection).
164
+ if (!signal?.aborted && closeCursor)
165
+ await cursor.close().catch(() => undefined);
166
+ }
167
+ }
168
+ function closesActiveCursorAfter(error) {
169
+ // A result-size rejection originates locally while the cursor portal is
170
+ // still active, so Close + Sync is required before ROLLBACK can proceed.
171
+ // PostgreSQL errors already emitted their own Sync; closing then can wait for
172
+ // a second ReadyForQuery that never arrives.
173
+ return error instanceof PostgresToolError && error.code === "RESULT_TOO_LARGE";
174
+ }
175
+ function queryCancelled() {
176
+ return new PostgresToolError("QUERY_CANCELLED", "La consulta fue cancelada.");
177
+ }
178
+ async function rejectUnsafeExecutionRole(client) {
179
+ const result = await client.query(`SELECT
180
+ role_info.rolsuper AS superuser,
181
+ COALESCE(
182
+ (
183
+ SELECT bool_or(
184
+ pg_has_role(current_user, dangerous_role.oid, 'USAGE')
185
+ OR pg_has_role(current_user, dangerous_role.oid, 'MEMBER')
186
+ )
187
+ FROM pg_roles dangerous_role
188
+ WHERE dangerous_role.rolname IN (
189
+ 'pg_signal_backend',
190
+ 'pg_signal_autovacuum_worker',
191
+ 'pg_read_server_files',
192
+ 'pg_write_server_files',
193
+ 'pg_execute_server_program'
194
+ )
195
+ ),
196
+ false
197
+ ) AS unsafe_server_role
198
+ FROM pg_roles role_info
199
+ WHERE role_info.rolname = current_user`);
200
+ const role = result.rows[0];
201
+ if (role === undefined) {
202
+ throw new PostgresToolError("DATABASE_ROLE_UNKNOWN", "No se pudo verificar la seguridad del rol PostgreSQL.");
203
+ }
204
+ if (role.superuser) {
205
+ throw new PostgresToolError("DATABASE_ROLE_UNSAFE", "La ejecución de tools requiere un rol PostgreSQL que no sea superusuario.");
206
+ }
207
+ if (role.unsafe_server_role) {
208
+ throw new PostgresToolError("DATABASE_ROLE_UNSAFE", "La ejecución de tools requiere un rol sin privilegios de servidor peligrosos.");
209
+ }
210
+ }
211
+ async function readCursorRows(cursor, signal) {
212
+ const rows = [];
213
+ // Track the JSON representation as rows arrive. `DatabaseToolCatalog` makes
214
+ // the final exact 4 MiB decision once it adds the canonical envelope, while
215
+ // this transport-level bound prevents a 1001-row cursor result from being
216
+ // accumulated in memory first.
217
+ let serializedBytes = 2; // []
218
+ while (rows.length <= MAX_TOOL_ROWS) {
219
+ const batch = await cursor.read(1);
220
+ if (signal?.aborted)
221
+ throw queryCancelled();
222
+ const row = batch[0];
223
+ if (row === undefined)
224
+ return { rows, truncated: false };
225
+ if (rows.length === MAX_TOOL_ROWS)
226
+ return { rows, truncated: true };
227
+ serializedBytes += serializedRowBytes(row) + (rows.length === 0 ? 0 : 1);
228
+ if (serializedBytes > MAX_TOOL_RESULT_BYTES) {
229
+ throw new PostgresToolError("RESULT_TOO_LARGE", "El resultado serializado supera 4 MiB.");
230
+ }
231
+ rows.push(row);
232
+ }
233
+ return { rows, truncated: true };
234
+ }
235
+ function serializedRowBytes(row) {
236
+ try {
237
+ return Buffer.byteLength(JSON.stringify(row), "utf8");
238
+ }
239
+ catch {
240
+ throw new PostgresToolError("RESULT_TOO_LARGE", "El resultado no se puede serializar de forma segura.");
241
+ }
242
+ }
243
+ function safeDriverError(error, connected) {
244
+ const code = readDriverCode(error);
245
+ if (!connected) {
246
+ return new PostgresToolError("DATABASE_CONNECTION_FAILED", "No se pudo abrir la conexión PostgreSQL.");
247
+ }
248
+ if (code === "42601") {
249
+ return new PostgresToolError("INVALID_SQL", "La consulta PostgreSQL tiene sintaxis inválida.");
250
+ }
251
+ if (code === "57014") {
252
+ return new PostgresToolError("QUERY_TIMEOUT", "La consulta excedió el límite de 30 segundos.");
253
+ }
254
+ if (code === "25006") {
255
+ return new PostgresToolError("READ_ONLY_POLICY", "PostgreSQL rechazó una operación fuera de la transacción de solo lectura.");
256
+ }
257
+ if (code?.startsWith("28") || code === "3D000") {
258
+ return new PostgresToolError("DATABASE_CONNECTION_FAILED", "No se pudo autenticar o abrir la base de datos.");
259
+ }
260
+ if (code === "ECONNREFUSED" ||
261
+ code === "ECONNRESET" ||
262
+ code === "ENETUNREACH" ||
263
+ code === "EHOSTUNREACH" ||
264
+ code === "ENOTFOUND" ||
265
+ code === "ETIMEDOUT" ||
266
+ code === "ERR_INVALID_URL") {
267
+ return new PostgresToolError("DATABASE_CONNECTION_FAILED", "No se pudo abrir la conexión PostgreSQL.");
268
+ }
269
+ return new PostgresToolError("DATABASE_QUERY_FAILED", "PostgreSQL no pudo completar la operación.");
270
+ }
271
+ function readDriverCode(error) {
272
+ if (error === null || typeof error !== "object")
273
+ return undefined;
274
+ const code = error.code;
275
+ return typeof code === "string" ? code : undefined;
276
+ }
277
+ //# sourceMappingURL=postgres-readonly-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres-readonly-tools.js","sourceRoot":"","sources":["../../src/adapters/postgres-readonly-tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,168 @@
1
+ import { encodeToolResponse, toolFailure } from "../domain/database-tools.js";
2
+ const PROTOCOL_VERSION = "2025-06-18";
3
+ /** JSON-RPC adapter over the shared catalog. It owns no tool rules. */
4
+ export function createDatabaseMcpServer(deps) {
5
+ let lifecycle = "new";
6
+ const activeCalls = new Map();
7
+ const cancelledBeforeStart = new Set();
8
+ function reply(id, result) {
9
+ if (!isJsonRpcResponseId(id))
10
+ return;
11
+ deps.send({ jsonrpc: "2.0", id, result });
12
+ }
13
+ function replyError(id, code, message) {
14
+ if (!isJsonRpcResponseId(id))
15
+ return;
16
+ deps.send({ jsonrpc: "2.0", id, error: { code, message } });
17
+ }
18
+ function toolResult(response) {
19
+ return {
20
+ content: [{ type: "text", text: encodeToolResponse(response), mimeType: "application/json" }],
21
+ ...(response.success ? {} : { isError: true }),
22
+ };
23
+ }
24
+ async function dispatchToolCall(message) {
25
+ const params = isRecord(message.params) ? message.params : {};
26
+ const name = typeof params.name === "string" ? params.name : "";
27
+ const requestKey = requestIdKey(message.id);
28
+ // tools/call is a request, not a notification. Rejecting an id-less call
29
+ // prevents an untracked database operation that the client cannot cancel.
30
+ if (requestKey === undefined) {
31
+ replyError(message.id, -32600, "tools/call requiere un id de solicitud válido");
32
+ return true;
33
+ }
34
+ if (cancelledBeforeStart.delete(requestKey)) {
35
+ reply(message.id, toolResult(toolFailure("QUERY_CANCELLED", "La consulta fue cancelada.")));
36
+ return true;
37
+ }
38
+ const controller = new AbortController();
39
+ activeCalls.set(requestKey, controller);
40
+ try {
41
+ const outcome = await deps.catalog.call({
42
+ tool: name,
43
+ connection: deps.connection,
44
+ input: params.arguments,
45
+ signal: controller.signal,
46
+ });
47
+ reply(message.id, toolResult(outcome.response));
48
+ }
49
+ catch {
50
+ reply(message.id, toolResult(toolFailure("TOOL_EXECUTION_FAILED", "La tool no pudo completar la solicitud.")));
51
+ }
52
+ finally {
53
+ if (activeCalls.get(requestKey) === controller) {
54
+ activeCalls.delete(requestKey);
55
+ }
56
+ }
57
+ return true;
58
+ }
59
+ async function cancelToolCall(message) {
60
+ const params = isRecord(message.params) ? message.params : {};
61
+ const key = requestIdKey(params.requestId);
62
+ if (key === undefined)
63
+ return true;
64
+ const active = activeCalls.get(key);
65
+ if (active !== undefined) {
66
+ active.abort();
67
+ return true;
68
+ }
69
+ // A stdio transport may receive cancellation while an earlier call keeps a
70
+ // later request queued. Retain bounded intent so that later call never
71
+ // reaches PostgreSQL once it starts.
72
+ cancelledBeforeStart.add(key);
73
+ if (cancelledBeforeStart.size > 1_000) {
74
+ const oldest = cancelledBeforeStart.values().next().value;
75
+ if (oldest !== undefined)
76
+ cancelledBeforeStart.delete(oldest);
77
+ }
78
+ return true;
79
+ }
80
+ async function initialize(message) {
81
+ if (lifecycle !== "new") {
82
+ replyError(message.id, -32600, "initialize sólo puede llamarse una vez por sesión");
83
+ return true;
84
+ }
85
+ lifecycle = "awaiting-initialized";
86
+ reply(message.id, {
87
+ protocolVersion: PROTOCOL_VERSION,
88
+ capabilities: { tools: {} },
89
+ serverInfo: { name: "agent-workflow-db", version: "1" },
90
+ });
91
+ return true;
92
+ }
93
+ async function initialized() {
94
+ if (lifecycle === "awaiting-initialized") {
95
+ lifecycle = "ready";
96
+ await deps.onHostLoadObserved?.();
97
+ }
98
+ return true;
99
+ }
100
+ function readyOrReply(message) {
101
+ if (lifecycle === "ready")
102
+ return true;
103
+ replyError(message.id, -32002, "Server not initialized");
104
+ return false;
105
+ }
106
+ async function listTools(message) {
107
+ if (!readyOrReply(message))
108
+ return true;
109
+ try {
110
+ const list = deps.catalog.list(deps.connection);
111
+ reply(message.id, "tools" in list.payload ? list.payload : toolResult(list.payload));
112
+ }
113
+ catch {
114
+ reply(message.id, toolResult(toolFailure("MCP_CONNECTION_INVALID", "No se pudo resolver la conexión solicitada.")));
115
+ }
116
+ return true;
117
+ }
118
+ async function callTool(message) {
119
+ if (!readyOrReply(message))
120
+ return true;
121
+ return await dispatchToolCall(message);
122
+ }
123
+ const handlers = {
124
+ initialize,
125
+ "notifications/initialized": async () => await initialized(),
126
+ "notifications/cancelled": async (message) => await cancelToolCall(message),
127
+ "tools/list": listTools,
128
+ "tools/call": callTool,
129
+ };
130
+ return {
131
+ async handle(message) {
132
+ if (!isJsonRpcEnvelope(message))
133
+ return false;
134
+ const handler = handlers[message.method];
135
+ if (handler !== undefined)
136
+ return await handler(message);
137
+ replyError(message.id, -32601, "Method not found");
138
+ return true;
139
+ },
140
+ };
141
+ }
142
+ /** Accept only envelopes for which every response remains valid JSON-RPC. */
143
+ function isJsonRpcEnvelope(value) {
144
+ return (isRecord(value) &&
145
+ value.jsonrpc === "2.0" &&
146
+ typeof value.method === "string" &&
147
+ isJsonRpcId(value.id));
148
+ }
149
+ function isJsonRpcId(value) {
150
+ return value === undefined || isJsonRpcResponseId(value);
151
+ }
152
+ function isJsonRpcResponseId(value) {
153
+ return value === null || typeof value === "string" || isFiniteId(value);
154
+ }
155
+ function isFiniteId(value) {
156
+ return typeof value === "number" && Number.isFinite(value);
157
+ }
158
+ function requestIdKey(value) {
159
+ if (typeof value === "string")
160
+ return `string:${value}`;
161
+ if (typeof value === "number" && Number.isFinite(value))
162
+ return `number:${value}`;
163
+ return undefined;
164
+ }
165
+ function isRecord(value) {
166
+ return value !== null && typeof value === "object" && !Array.isArray(value);
167
+ }
168
+ //# sourceMappingURL=database-mcp-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database-mcp-server.js","sourceRoot":"","sources":["../../src/application/database-mcp-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ import { StringDecoder } from "node:string_decoder";
2
+ import { MAX_TOOL_TRANSPORT_INPUT_BYTES } from "../domain/database-tools.js";
3
+ import { createDatabaseMcpServer } from "./database-mcp-server.js";
4
+ // The RPC envelope is allowed a small fixed overhead beyond the canonical 1
5
+ // MiB tool input. The shared catalog still makes the exact input decision.
6
+ const MAX_MCP_INPUT_FRAME_BYTES = MAX_TOOL_TRANSPORT_INPUT_BYTES;
7
+ /**
8
+ * Line-framed stdio transport. stdout is exclusively JSON-RPC; malformed input
9
+ * is diagnosed on stderr and cannot corrupt a later valid request.
10
+ */
11
+ export function runDatabaseMcpStdio(deps) {
12
+ const server = createDatabaseMcpServer({
13
+ send: (message) => deps.output.write(`${JSON.stringify(message)}\n`),
14
+ catalog: deps.catalog,
15
+ connection: deps.connection,
16
+ ...(deps.onHostLoadObserved === undefined
17
+ ? {}
18
+ : { onHostLoadObserved: deps.onHostLoadObserved }),
19
+ });
20
+ const frames = {
21
+ buffer: "",
22
+ bufferedBytes: 0,
23
+ discardingOversizedLine: false,
24
+ decoder: new StringDecoder("utf8"),
25
+ };
26
+ let queue = Promise.resolve();
27
+ function enqueue(line) {
28
+ let message;
29
+ try {
30
+ message = JSON.parse(line);
31
+ }
32
+ catch {
33
+ deps.diagnostics.write("aw mcp serve-db: línea JSON-RPC inválida descartada\n");
34
+ return;
35
+ }
36
+ // Cancellation must not wait behind a running database call in the serial
37
+ // queue. All other frames retain order, including the lifecycle sequence.
38
+ if (isCancellationNotification(message)) {
39
+ void server.handle(message).catch(() => {
40
+ deps.diagnostics.write("aw mcp serve-db: línea JSON-RPC inválida descartada\n");
41
+ });
42
+ return;
43
+ }
44
+ queue = queue.then(async () => {
45
+ try {
46
+ await server.handle(message);
47
+ }
48
+ catch {
49
+ deps.diagnostics.write("aw mcp serve-db: línea JSON-RPC inválida descartada\n");
50
+ }
51
+ });
52
+ }
53
+ return new Promise((resolve) => {
54
+ let ended = false;
55
+ const finish = () => {
56
+ if (ended)
57
+ return;
58
+ ended = true;
59
+ // An unterminated tail is not a JSON-RPC frame. Do not enqueue it, but
60
+ // flush the decoder so no multibyte bytes remain retained by the stream.
61
+ if (!frames.discardingOversizedLine)
62
+ frames.buffer += frames.decoder.end();
63
+ void queue.then(() => resolve());
64
+ };
65
+ deps.input.on("data", (chunk) => {
66
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
67
+ enqueueConsumption(consumeChunk(frames, bytes), deps, enqueue);
68
+ });
69
+ deps.input.on("end", finish);
70
+ deps.input.on("close", finish);
71
+ });
72
+ }
73
+ function isCancellationNotification(message) {
74
+ return (message !== null &&
75
+ typeof message === "object" &&
76
+ !Array.isArray(message) &&
77
+ message.method === "notifications/cancelled");
78
+ }
79
+ /**
80
+ * Splits raw bytes at newline boundaries before decoding. This matters for an
81
+ * oversized frame already terminated in the same chunk: appending/splitting a
82
+ * decoded 100 MiB string merely to reject it would defeat the input bound.
83
+ */
84
+ function consumeChunk(state, bytes) {
85
+ const frames = [];
86
+ let oversizedFrames = 0;
87
+ let offset = 0;
88
+ while (offset < bytes.length) {
89
+ const newline = bytes.indexOf(0x0a, offset);
90
+ const end = newline < 0 ? bytes.length : newline + 1;
91
+ const segment = bytes.subarray(offset, end);
92
+ if (state.discardingOversizedLine) {
93
+ if (newline >= 0) {
94
+ state.discardingOversizedLine = false;
95
+ state.decoder = new StringDecoder("utf8");
96
+ }
97
+ offset = end;
98
+ continue;
99
+ }
100
+ if (state.bufferedBytes + segment.length > MAX_MCP_INPUT_FRAME_BYTES) {
101
+ state.buffer = "";
102
+ state.bufferedBytes = 0;
103
+ state.decoder = new StringDecoder("utf8");
104
+ state.discardingOversizedLine = newline < 0;
105
+ oversizedFrames += 1;
106
+ offset = end;
107
+ continue;
108
+ }
109
+ state.bufferedBytes += segment.length;
110
+ state.buffer += state.decoder.write(segment);
111
+ if (newline >= 0)
112
+ consumeCompleteLine(state, frames);
113
+ offset = end;
114
+ }
115
+ return { frames, oversizedFrames };
116
+ }
117
+ function consumeCompleteLine(state, frames) {
118
+ const newline = state.buffer.indexOf("\n");
119
+ if (newline < 0)
120
+ return;
121
+ const trimmed = state.buffer.slice(0, newline).replace(/\r$/, "").trim();
122
+ state.buffer = state.buffer.slice(newline + 1);
123
+ state.bufferedBytes = 0;
124
+ if (trimmed.length > 0)
125
+ frames.push(trimmed);
126
+ }
127
+ function reportOversizedFrames(output, count) {
128
+ for (let index = 0; index < count; index += 1) {
129
+ output.write("aw mcp serve-db: frame JSON-RPC demasiado grande descartado\n");
130
+ }
131
+ }
132
+ function enqueueConsumption(consumed, deps, enqueue) {
133
+ reportOversizedFrames(deps.diagnostics, consumed.oversizedFrames);
134
+ for (const frame of consumed.frames)
135
+ enqueue(frame);
136
+ }
137
+ //# sourceMappingURL=database-mcp-stdio.js.map