@happyvertical/smrt-dev-mcp 0.46.0 → 0.47.1
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/AGENTS.md +6 -0
- package/README.md +113 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +428 -2
- package/dist/index.js.map +1 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/{knowledge-SqpqYA67.js → knowledge-DmZ-7GO9.js} +105 -2
- package/dist/knowledge-DmZ-7GO9.js.map +1 -0
- package/dist/knowledge.js +1 -1
- package/dist/tool-catalog.d.ts.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/runtime/connection.d.ts +70 -0
- package/dist/tools/runtime/connection.d.ts.map +1 -0
- package/dist/tools/runtime/tools.d.ts +48 -0
- package/dist/tools/runtime/tools.d.ts.map +1 -0
- package/package.json +7 -5
- package/dist/knowledge-SqpqYA67.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -23,6 +23,12 @@ deterministic SMRT ecosystem knowledge, and portable agent workflows.
|
|
|
23
23
|
| `smrt-architecture` | Recommends SMRT/SDK packages, object-model sketch, risks, and questions |
|
|
24
24
|
| `list-agent-skills` | Lists bundled harness-agnostic agent skills |
|
|
25
25
|
| `get-agent-skill` | Returns a bundled agent skill as Markdown plus optional references |
|
|
26
|
+
| `migration-status` | Live migration status from the `_smrt_schema_migrations` system table — completed/running/failed/rolled_back counts, latest completed and failed migrations (runtime provenance; read-only; optional live DB, #1824) |
|
|
27
|
+
| `job-health` | Live job queue health from the `_smrt_jobs`/`_smrt_workers` system tables — counts by status, stuck/failed jobs, worker liveness; payloads/results never read (runtime provenance; read-only, #1824) |
|
|
28
|
+
| `schedule-health` | Live agent schedule health from the `_smrt_agent_schedules` system table — due/overdue/errored counts, last/next run; sensitive `agentConfig` never read (runtime provenance; read-only, #1824) |
|
|
29
|
+
| `dispatch-health` | Live dispatch health from the `_smrt_dispatch`/`_smrt_dispatch_subscriptions` system tables — stuck/pending by type/status, subscription topology; payloads/metadata never read (runtime provenance; read-only, #1824) |
|
|
30
|
+
| `recent-changes` | Tail of the `_smrt_changes` append-only change feed with cursor semantics, filterable by table and tenant (runtime provenance; read-only, #1824) |
|
|
31
|
+
| `registry-drift` | Registry drift report — `_smrt_registry` is retired and reported as such, never queried or fabricated (read-only, #1824) |
|
|
26
32
|
|
|
27
33
|
`pnpm knowledge:check --strict --format markdown` compares this catalog and the
|
|
28
34
|
README parameter tables to the exported `TOOLS` definitions. Add or change a
|
package/README.md
CHANGED
|
@@ -456,6 +456,119 @@ Return a bundled agent skill as Markdown, with optional referenced files.
|
|
|
456
456
|
| `name` | `'smrt-code-review'` | Yes | Skill name |
|
|
457
457
|
| `includeReferences` | `boolean` | No | Include referenced files (default: true) |
|
|
458
458
|
|
|
459
|
+
### `migration-status`
|
|
460
|
+
|
|
461
|
+
Live migration status from the `_smrt_schema_migrations` system table:
|
|
462
|
+
completed / running / failed / rolled-back counts (the tracker's real status
|
|
463
|
+
vocabulary) plus the latest completed and the failed migrations. Runtime
|
|
464
|
+
provenance (`runtime (live DB)`); read-only. Without a configured connection it
|
|
465
|
+
returns a successful static-only result.
|
|
466
|
+
|
|
467
|
+
| Parameter | Type | Required | Description |
|
|
468
|
+
|-----------|------|----------|-------------|
|
|
469
|
+
| `dbUrl` | `string` | No | Optional dev database URL override (read-only diagnostics); prefer `SMRT_DEV_DB_URL` or `cli.database` config |
|
|
470
|
+
| `dbType` | `'sqlite' \| 'postgres' \| 'duckdb'` | No | Optional engine hint for `dbUrl` or the environment connection; inferred from the URL scheme when omitted |
|
|
471
|
+
| `limit` | `number` | No | Row budget for result lists (default 50, capped at 500) |
|
|
472
|
+
|
|
473
|
+
### `job-health`
|
|
474
|
+
|
|
475
|
+
Live job queue health from the `_smrt_jobs`, `_smrt_workers`, and
|
|
476
|
+
`_smrt_job_events` system tables: counts by status, stuck/failed jobs, worker
|
|
477
|
+
liveness. Job payloads and results are never read. Runtime provenance;
|
|
478
|
+
read-only.
|
|
479
|
+
|
|
480
|
+
| Parameter | Type | Required | Description |
|
|
481
|
+
|-----------|------|----------|-------------|
|
|
482
|
+
| `dbUrl` | `string` | No | Optional dev database URL override (read-only diagnostics); prefer `SMRT_DEV_DB_URL` or `cli.database` config |
|
|
483
|
+
| `dbType` | `'sqlite' \| 'postgres' \| 'duckdb'` | No | Optional engine hint for `dbUrl` or the environment connection; inferred from the URL scheme when omitted |
|
|
484
|
+
| `limit` | `number` | No | Row budget for result lists (default 50, capped at 500) |
|
|
485
|
+
|
|
486
|
+
### `schedule-health`
|
|
487
|
+
|
|
488
|
+
Live agent schedule health from the `_smrt_agent_schedules` system table:
|
|
489
|
+
due/overdue/errored counts, last/next run per schedule. The sensitive
|
|
490
|
+
`agentConfig`/`methodArgs` columns are never read. Runtime provenance;
|
|
491
|
+
read-only.
|
|
492
|
+
|
|
493
|
+
| Parameter | Type | Required | Description |
|
|
494
|
+
|-----------|------|----------|-------------|
|
|
495
|
+
| `dbUrl` | `string` | No | Optional dev database URL override (read-only diagnostics); prefer `SMRT_DEV_DB_URL` or `cli.database` config |
|
|
496
|
+
| `dbType` | `'sqlite' \| 'postgres' \| 'duckdb'` | No | Optional engine hint for `dbUrl` or the environment connection; inferred from the URL scheme when omitted |
|
|
497
|
+
| `limit` | `number` | No | Row budget for result lists (default 50, capped at 500) |
|
|
498
|
+
|
|
499
|
+
### `dispatch-health`
|
|
500
|
+
|
|
501
|
+
Live dispatch health from the `_smrt_dispatch` and
|
|
502
|
+
`_smrt_dispatch_subscriptions` system tables: stuck/pending messages by type
|
|
503
|
+
and status plus subscription topology. Dispatch payloads and metadata are never
|
|
504
|
+
read. Runtime provenance; read-only.
|
|
505
|
+
|
|
506
|
+
| Parameter | Type | Required | Description |
|
|
507
|
+
|-----------|------|----------|-------------|
|
|
508
|
+
| `dbUrl` | `string` | No | Optional dev database URL override (read-only diagnostics); prefer `SMRT_DEV_DB_URL` or `cli.database` config |
|
|
509
|
+
| `dbType` | `'sqlite' \| 'postgres' \| 'duckdb'` | No | Optional engine hint for `dbUrl` or the environment connection; inferred from the URL scheme when omitted |
|
|
510
|
+
| `limit` | `number` | No | Row budget for result lists (default 50, capped at 500) |
|
|
511
|
+
|
|
512
|
+
### `recent-changes`
|
|
513
|
+
|
|
514
|
+
Tail of the `_smrt_changes` append-only change feed with cursor semantics,
|
|
515
|
+
filterable by table and tenant. Runtime provenance; read-only.
|
|
516
|
+
|
|
517
|
+
| Parameter | Type | Required | Description |
|
|
518
|
+
|-----------|------|----------|-------------|
|
|
519
|
+
| `dbUrl` | `string` | No | Optional dev database URL override (read-only diagnostics); prefer `SMRT_DEV_DB_URL` or `cli.database` config |
|
|
520
|
+
| `dbType` | `'sqlite' \| 'postgres' \| 'duckdb'` | No | Optional engine hint for `dbUrl` or the environment connection; inferred from the URL scheme when omitted |
|
|
521
|
+
| `since` | `number` | No | Cursor to read after (default 0) |
|
|
522
|
+
| `tables` | `string[]` | No | Restrict to these physical table names |
|
|
523
|
+
| `tenantId` | `string` | No | Tenant narrowing filter |
|
|
524
|
+
| `limit` | `number` | No | Page size (default 200, capped at 500) |
|
|
525
|
+
|
|
526
|
+
### `registry-drift`
|
|
527
|
+
|
|
528
|
+
Registry drift report. `_smrt_registry` is retired (system schema 1.10.1) and
|
|
529
|
+
is never queried: the tool reports the retirement and whether a legacy empty
|
|
530
|
+
table remains, and never fabricates drift. Declared objects come from the
|
|
531
|
+
static manifest tools. Read-only.
|
|
532
|
+
|
|
533
|
+
| Parameter | Type | Required | Description |
|
|
534
|
+
|-----------|------|----------|-------------|
|
|
535
|
+
| `dbUrl` | `string` | No | Optional dev database URL override (read-only diagnostics); prefer `SMRT_DEV_DB_URL` or `cli.database` config |
|
|
536
|
+
| `dbType` | `'sqlite' \| 'postgres' \| 'duckdb'` | No | Optional engine hint for `dbUrl` or the environment connection; inferred from the URL scheme when omitted |
|
|
537
|
+
|
|
538
|
+
## Runtime Diagnostics (Optional Live DB)
|
|
539
|
+
|
|
540
|
+
The six runtime-diagnostics tools above (`migration-status`, `job-health`,
|
|
541
|
+
`schedule-health`, `dispatch-health`, `recent-changes`, `registry-drift`) share
|
|
542
|
+
an **optional, read-only** dev-database connection so a development agent can
|
|
543
|
+
see runtime truth — not just what static artifacts declare. They complement the
|
|
544
|
+
static knowledge tools (#1819); together the two surfaces give the full
|
|
545
|
+
picture.
|
|
546
|
+
|
|
547
|
+
**Configuration.** Connection resolution, per call: an explicit `dbUrl`
|
|
548
|
+
argument, then the `SMRT_DEV_DB_URL` environment variable, then the project's
|
|
549
|
+
smrt config `cli.database` (`database.type` / `database.url`). An optional
|
|
550
|
+
`dbType` argument (`sqlite`, `postgres`, `duckdb`) overrides engine inference
|
|
551
|
+
for `dbUrl` or the environment connection; unknown values fail with a safe
|
|
552
|
+
diagnostic instead of opening the wrong adapter. No configuration
|
|
553
|
+
anywhere → every runtime tool returns a successful static-only result
|
|
554
|
+
(`provenance: 'static'`, `connected: false`) and the server starts and serves
|
|
555
|
+
all static tools unaffected.
|
|
556
|
+
|
|
557
|
+
**Provenance labeling.** Live results carry `provenance: 'runtime (live DB)'`;
|
|
558
|
+
static-only results carry `provenance: 'static'`. Never conflate the two: the
|
|
559
|
+
manifest reports what the code declares, runtime tools report what the running
|
|
560
|
+
system is doing.
|
|
561
|
+
|
|
562
|
+
**Read-only, dev-only.** Every runtime statement is a bounded `SELECT` with an
|
|
563
|
+
explicit safe column projection. Sensitive columns (job payloads/results,
|
|
564
|
+
schedule `agentConfig`/`methodArgs`, dispatch `payload`/`metadata`) are never
|
|
565
|
+
selected. This is a dev/localhost diagnostic surface, not a data plane — it is
|
|
566
|
+
not `smrt-app-mcp` and never writes.
|
|
567
|
+
|
|
568
|
+
**Connection strings are never logged.** Surfaced URLs are redacted
|
|
569
|
+
(passwords and token query params masked); driver errors pass through a
|
|
570
|
+
redacting normalizer before they can appear in a diagnostic.
|
|
571
|
+
|
|
459
572
|
## MCP Resources And Prompts
|
|
460
573
|
|
|
461
574
|
Resources:
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAGL,MAAM,EACP,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAGL,MAAM,EACP,MAAM,8BAA8B,CAAC;AA+BtC,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAG1C,eAAO,MAAM,cAAc,QAAuB,CAAC;AA6BnD,wBAAgB,YAAY,IAAI,MAAM,CA4qBrC"}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as checkKnowledgeFreshness, f as smrtArchitecture, h as TOOLS, i as buildReviewContext, m as REVIEW_SKILL_NAME, n as buildKnowledgeIndex, o as checkKnowledgeFreshnessFromIndex, p as smrtReview, r as buildPackageSpecialistContext, s as compactContextResult, t as buildArchitectureContext } from "./knowledge-
|
|
2
|
+
import { a as checkKnowledgeFreshness, f as smrtArchitecture, h as TOOLS, i as buildReviewContext, m as REVIEW_SKILL_NAME, n as buildKnowledgeIndex, o as checkKnowledgeFreshnessFromIndex, p as smrtReview, r as buildPackageSpecialistContext, s as compactContextResult, t as buildArchitectureContext } from "./knowledge-DmZ-7GO9.js";
|
|
3
3
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { ProtocolError, ProtocolErrorCode, Server } from "@modelcontextprotocol/server";
|
|
7
7
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
import { ManifestAdapter, OxcScanner } from "@happyvertical/smrt-scanner";
|
|
9
10
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
10
11
|
import { ManifestGenerator } from "@happyvertical/smrt-core/scanner";
|
|
12
|
+
import { readDispatchHealth, readJobHealth, readMigrationStatus, readRecentChanges, readRegistryDrift, readScheduleHealth } from "@happyvertical/smrt-core";
|
|
13
|
+
import { getPackageConfig, loadConfig } from "@happyvertical/smrt-config";
|
|
14
|
+
import { getDatabase } from "@happyvertical/sql";
|
|
11
15
|
//#region src/agent-skills.ts
|
|
12
16
|
var AGENT_SKILLS = [{
|
|
13
17
|
name: "smrt-code-review",
|
|
@@ -1052,6 +1056,395 @@ function pathExistsSyncHint(path) {
|
|
|
1052
1056
|
return existsSync(path);
|
|
1053
1057
|
}
|
|
1054
1058
|
//#endregion
|
|
1059
|
+
//#region src/tools/runtime/connection.ts
|
|
1060
|
+
/**
|
|
1061
|
+
* Optional read-only dev-database connection resolution for runtime
|
|
1062
|
+
* diagnostics tools (#1824).
|
|
1063
|
+
*
|
|
1064
|
+
* Resolution order per call:
|
|
1065
|
+
* 1. explicit `dbUrl`/`dbType` tool arguments
|
|
1066
|
+
* 2. `SMRT_DEV_DB_URL` environment variable
|
|
1067
|
+
* 3. the project's cosmiconfig CLI section (`getPackageConfig('cli', ...)`
|
|
1068
|
+
* from `@happyvertical/smrt-config`) → `database.{type,url}`
|
|
1069
|
+
*
|
|
1070
|
+
* No configured connection → `db: null` with `source: 'none'`; callers return
|
|
1071
|
+
* a successful static-only envelope. A connection is always opened lazily per
|
|
1072
|
+
* call and closed in the caller's `finally` — nothing is cached across calls
|
|
1073
|
+
* and the server never holds a database handle.
|
|
1074
|
+
*
|
|
1075
|
+
* Sensitive handling: connection strings are never logged or echoed. Every
|
|
1076
|
+
* surfaced URL passes through {@link redactConnectionString}; driver errors
|
|
1077
|
+
* are surfaced only through {@link safeErrorMessage}, which strips anything
|
|
1078
|
+
* that looks like a credential-bearing URL.
|
|
1079
|
+
*/
|
|
1080
|
+
var RUNTIME_DATABASE_TYPES = [
|
|
1081
|
+
"sqlite",
|
|
1082
|
+
"postgres",
|
|
1083
|
+
"duckdb"
|
|
1084
|
+
];
|
|
1085
|
+
function isRuntimeDatabaseType(value) {
|
|
1086
|
+
return RUNTIME_DATABASE_TYPES.includes(value);
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Sensitive query-parameter names. Matching normalizes the key (lowercase,
|
|
1090
|
+
* `_`/`-` stripped), so camelCase (`authToken`, `accessToken`) and hyphen
|
|
1091
|
+
* variants (`api-key`) are masked exactly like their snake_case forms.
|
|
1092
|
+
*/
|
|
1093
|
+
var SENSITIVE_QUERY_PARAMS = [
|
|
1094
|
+
"access_token",
|
|
1095
|
+
"apikey",
|
|
1096
|
+
"api_key",
|
|
1097
|
+
"auth",
|
|
1098
|
+
"auth_token",
|
|
1099
|
+
"connectionstring",
|
|
1100
|
+
"connection_string",
|
|
1101
|
+
"password",
|
|
1102
|
+
"token"
|
|
1103
|
+
];
|
|
1104
|
+
function normalizeQueryParamName(key) {
|
|
1105
|
+
return key.toLowerCase().replace(/[_-]/g, "");
|
|
1106
|
+
}
|
|
1107
|
+
var SENSITIVE_QUERY_PARAM_NAMES = new Set(SENSITIVE_QUERY_PARAMS.map(normalizeQueryParamName));
|
|
1108
|
+
var DEFAULT_CLI_DATABASE = { database: {
|
|
1109
|
+
type: "sqlite",
|
|
1110
|
+
url: ":memory:"
|
|
1111
|
+
} };
|
|
1112
|
+
/**
|
|
1113
|
+
* Redact a connection string so it can be shown to an agent without leaking
|
|
1114
|
+
* credentials. Mirrors the CLI's `redactConnectionString` (which is CLI
|
|
1115
|
+
* private); patterned identically so dev-mcp never depends on the CLI.
|
|
1116
|
+
*
|
|
1117
|
+
* Query-parameter masking normalizes each key (lowercase, `_`/`-` stripped),
|
|
1118
|
+
* so camelCase forms such as Turso/libsql's `?authToken=` mask exactly like
|
|
1119
|
+
* their snake_case forms. A final regex pass also masks `key=value` pairs
|
|
1120
|
+
* embedded in free text (driver error messages often quote the URL); it treats
|
|
1121
|
+
* the start of the string, `?`, `&`, `,`, `(`, and whitespace as the
|
|
1122
|
+
* preceding boundary.
|
|
1123
|
+
*/
|
|
1124
|
+
function redactConnectionString(value) {
|
|
1125
|
+
let redacted = value;
|
|
1126
|
+
try {
|
|
1127
|
+
const url = new URL(value);
|
|
1128
|
+
if (url.password) url.password = "***";
|
|
1129
|
+
for (const key of [...url.searchParams.keys()]) if (SENSITIVE_QUERY_PARAM_NAMES.has(normalizeQueryParamName(key))) url.searchParams.set(key, "***");
|
|
1130
|
+
redacted = url.toString();
|
|
1131
|
+
} catch {
|
|
1132
|
+
redacted = value.replace(/([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)(?:[^@\s]|@(?=[^@\s]*@))+(@)/gi, "$1***$2");
|
|
1133
|
+
}
|
|
1134
|
+
return redacted.replace(/((?:^|[?&,(\s])([a-z][a-z0-9_-]{0,30})=)([^&,\s)]+)/gi, (match, prefix, key) => SENSITIVE_QUERY_PARAM_NAMES.has(normalizeQueryParamName(key)) ? `${prefix}***` : match);
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Build a safe, redacted error message for a database failure. Connection
|
|
1138
|
+
* strings and raw driver error objects are never surfaced verbatim.
|
|
1139
|
+
*/
|
|
1140
|
+
function safeErrorMessage(error) {
|
|
1141
|
+
return redactConnectionString(error instanceof Error ? error.message : String(error ?? "unknown error"));
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Normalize a type hint into an engine `getDatabase` accepts. Unknown values
|
|
1145
|
+
* throw a safe error (no URL is included) so the caller can surface a
|
|
1146
|
+
* diagnostic instead of silently opening the wrong adapter.
|
|
1147
|
+
*/
|
|
1148
|
+
function toRuntimeDatabaseType(value) {
|
|
1149
|
+
const normalized = value.trim().toLowerCase();
|
|
1150
|
+
if (isRuntimeDatabaseType(normalized)) return normalized;
|
|
1151
|
+
throw new Error(`Unsupported runtime database type "${normalized}"; expected sqlite, postgres, or duckdb`);
|
|
1152
|
+
}
|
|
1153
|
+
/** Infer an engine hint from a URL scheme when no explicit type is given. */
|
|
1154
|
+
function inferDatabaseType(url, hint) {
|
|
1155
|
+
if (hint && hint.trim().length > 0) return hint;
|
|
1156
|
+
if (/^postgres(ql)?:/i.test(url)) return "postgres";
|
|
1157
|
+
if (/^duckdb:/i.test(url)) return "duckdb";
|
|
1158
|
+
return "sqlite";
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Resolve the dev-database connection for one tool call.
|
|
1162
|
+
*
|
|
1163
|
+
* Returns `db: null` (never throws) when no connection is configured; callers
|
|
1164
|
+
* must treat that as "no runtime database" and return a static-only envelope.
|
|
1165
|
+
* A thrown connect error is propagated to the caller, which converts it into
|
|
1166
|
+
* a diagnostic envelope — it must never reach the MCP transport.
|
|
1167
|
+
*/
|
|
1168
|
+
async function resolveRuntimeConnection(args = {}) {
|
|
1169
|
+
const argUrl = args.dbUrl?.trim();
|
|
1170
|
+
if (argUrl && argUrl !== ":memory:") {
|
|
1171
|
+
const databaseType = toRuntimeDatabaseType(inferDatabaseType(argUrl, args.dbType));
|
|
1172
|
+
return {
|
|
1173
|
+
db: await getDatabaseInstance({
|
|
1174
|
+
type: databaseType,
|
|
1175
|
+
url: argUrl
|
|
1176
|
+
}),
|
|
1177
|
+
source: "argument",
|
|
1178
|
+
displayUrl: redactConnectionString(argUrl),
|
|
1179
|
+
databaseType
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
const envUrl = process.env.SMRT_DEV_DB_URL?.trim();
|
|
1183
|
+
if (envUrl && envUrl !== ":memory:") {
|
|
1184
|
+
const databaseType = toRuntimeDatabaseType(inferDatabaseType(envUrl, args.dbType));
|
|
1185
|
+
return {
|
|
1186
|
+
db: await getDatabaseInstance({
|
|
1187
|
+
type: databaseType,
|
|
1188
|
+
url: envUrl
|
|
1189
|
+
}),
|
|
1190
|
+
source: "environment",
|
|
1191
|
+
displayUrl: redactConnectionString(envUrl),
|
|
1192
|
+
databaseType
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const config = await loadCliDatabaseConfig();
|
|
1196
|
+
const configUrl = config?.database?.url?.trim();
|
|
1197
|
+
if (configUrl && configUrl !== ":memory:") {
|
|
1198
|
+
const databaseType = toRuntimeDatabaseType(config.database?.type || inferDatabaseType(configUrl, args.dbType));
|
|
1199
|
+
return {
|
|
1200
|
+
db: await getDatabaseInstance({
|
|
1201
|
+
type: databaseType,
|
|
1202
|
+
url: configUrl
|
|
1203
|
+
}),
|
|
1204
|
+
source: "config",
|
|
1205
|
+
displayUrl: redactConnectionString(configUrl),
|
|
1206
|
+
databaseType
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
db: null,
|
|
1211
|
+
source: "none",
|
|
1212
|
+
displayUrl: "",
|
|
1213
|
+
databaseType: null
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
async function loadCliDatabaseConfig() {
|
|
1217
|
+
try {
|
|
1218
|
+
await loadConfig();
|
|
1219
|
+
const database = getPackageConfig("cli", DEFAULT_CLI_DATABASE).database;
|
|
1220
|
+
if (database && typeof database.url === "string") return { database };
|
|
1221
|
+
return {};
|
|
1222
|
+
} catch {
|
|
1223
|
+
return {};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async function getDatabaseInstance(options) {
|
|
1227
|
+
return getDatabase(options);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Best-effort close of a resolved connection. Never throws; diagnostics must
|
|
1231
|
+
* not fail because cleanup hiccuped.
|
|
1232
|
+
*/
|
|
1233
|
+
async function closeRuntimeConnection(db) {
|
|
1234
|
+
if (!db || typeof db !== "object") return;
|
|
1235
|
+
const closeable = db;
|
|
1236
|
+
const close = closeable.close ?? closeable.client?.end ?? closeable.client?.close;
|
|
1237
|
+
if (typeof close !== "function") return;
|
|
1238
|
+
try {
|
|
1239
|
+
await close.call(closeable.close ? closeable : closeable.client);
|
|
1240
|
+
} catch {}
|
|
1241
|
+
}
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/tools/runtime/tools.ts
|
|
1244
|
+
/**
|
|
1245
|
+
* Runtime diagnostics tools (#1824): read-only views over a project's dev
|
|
1246
|
+
* database `_smrt_*` system tables, powered by the shared SELECT-only
|
|
1247
|
+
* system-diagnostics reader in `@happyvertical/smrt-core`.
|
|
1248
|
+
*
|
|
1249
|
+
* Contract:
|
|
1250
|
+
* - **Optional connection.** No configured connection returns a successful
|
|
1251
|
+
* static-only envelope — the server always starts and static tools are
|
|
1252
|
+
* unaffected. A live connection is opened lazily per call and closed in
|
|
1253
|
+
* `finally`; nothing is cached across calls.
|
|
1254
|
+
* - **Read-only.** Every underlying statement is a bounded SELECT; the reader
|
|
1255
|
+
* never selects sensitive columns (job payloads/results, schedule
|
|
1256
|
+
* `agentConfig`/`methodArgs`, dispatch `payload`/`metadata`).
|
|
1257
|
+
* - **Provenance-labeled.** Live results carry `provenance: 'runtime (live DB)'`;
|
|
1258
|
+
* static-only results carry `provenance: 'static'` — agents must never
|
|
1259
|
+
* conflate runtime facts with declared/manifest facts.
|
|
1260
|
+
* - **Fail-safe.** A connect/read error becomes a diagnostic envelope; it must
|
|
1261
|
+
* never reach the MCP transport and never includes raw driver text or URLs.
|
|
1262
|
+
*/
|
|
1263
|
+
/** Provenance labels separating runtime facts from static/declared facts. */
|
|
1264
|
+
var RUNTIME_PROVENANCE = "runtime (live DB)";
|
|
1265
|
+
var STATIC_PROVENANCE = "static";
|
|
1266
|
+
/**
|
|
1267
|
+
* Serialize resolve → read → close per connection target so overlapping tool
|
|
1268
|
+
* calls never close a shared cached handle out from under each other.
|
|
1269
|
+
*
|
|
1270
|
+
* `@happyvertical/sql`'s `getDatabase` returns a cached handle per URL (no
|
|
1271
|
+
* opt-out in its public API). `closeRuntimeConnection` only calls the handle's
|
|
1272
|
+
* own `close`/`end`, but the SDK wraps those so a close also evicts the handle
|
|
1273
|
+
* from its connection cache. Two concurrent diagnostics calls resolving the
|
|
1274
|
+
* same URL would therefore share one handle, with the first finisher closing
|
|
1275
|
+
* it mid-read for the second. A per-key promise chain keeps each call's
|
|
1276
|
+
* lifecycle private: every call resolves its own view of the connection,
|
|
1277
|
+
* performs its read, and only then closes — the next queued call re-resolves
|
|
1278
|
+
* a fresh handle.
|
|
1279
|
+
*
|
|
1280
|
+
* The key is a digest of the resolved target, never the raw URL, so a
|
|
1281
|
+
* credential-bearing connection string is not retained in this map.
|
|
1282
|
+
*/
|
|
1283
|
+
var runtimeReadQueues = /* @__PURE__ */ new Map();
|
|
1284
|
+
function connectionQueueKey(args) {
|
|
1285
|
+
const target = args.dbUrl?.trim() || process.env.SMRT_DEV_DB_URL?.trim() || "cli.config";
|
|
1286
|
+
return createHash("sha256").update(target).digest("hex");
|
|
1287
|
+
}
|
|
1288
|
+
async function enqueueRuntimeRead(key, operation) {
|
|
1289
|
+
const run = (runtimeReadQueues.get(key) ?? Promise.resolve()).then(operation, operation);
|
|
1290
|
+
const tail = run.catch(() => void 0);
|
|
1291
|
+
runtimeReadQueues.set(key, tail);
|
|
1292
|
+
tail.then(() => {
|
|
1293
|
+
if (runtimeReadQueues.get(key) === tail) runtimeReadQueues.delete(key);
|
|
1294
|
+
});
|
|
1295
|
+
return run;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Run one read against the optional runtime connection, mapping every outcome
|
|
1299
|
+
* to a successful MCP envelope:
|
|
1300
|
+
*
|
|
1301
|
+
* - no connection configured → static-only envelope (`connected: false`)
|
|
1302
|
+
* - connect failure → static envelope with a safe diagnostic
|
|
1303
|
+
* - read failure → connected envelope with a safe diagnostic
|
|
1304
|
+
* - success → live result under `provenance: 'runtime (live DB)'`; a
|
|
1305
|
+
* category-unavailable reader result keeps its `available: false` data and
|
|
1306
|
+
* surfaces its message as a diagnostic
|
|
1307
|
+
*/
|
|
1308
|
+
async function withRuntimeConnection(args, read, staticHint) {
|
|
1309
|
+
return enqueueRuntimeRead(connectionQueueKey(args), () => runWithRuntimeConnection(args, read, staticHint));
|
|
1310
|
+
}
|
|
1311
|
+
async function runWithRuntimeConnection(args, read, staticHint) {
|
|
1312
|
+
let resolved;
|
|
1313
|
+
try {
|
|
1314
|
+
resolved = await resolveRuntimeConnection(args);
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
return {
|
|
1317
|
+
ok: true,
|
|
1318
|
+
coverage: null,
|
|
1319
|
+
diagnostics: [{
|
|
1320
|
+
severity: "warning",
|
|
1321
|
+
code: "runtime_connection_error",
|
|
1322
|
+
message: safeErrorMessage(error)
|
|
1323
|
+
}],
|
|
1324
|
+
data: {
|
|
1325
|
+
provenance: STATIC_PROVENANCE,
|
|
1326
|
+
connected: false
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
if (!resolved.db) return {
|
|
1331
|
+
ok: true,
|
|
1332
|
+
coverage: null,
|
|
1333
|
+
diagnostics: [{
|
|
1334
|
+
severity: "info",
|
|
1335
|
+
code: "runtime_connection_unavailable",
|
|
1336
|
+
message: `No runtime dev database configured (set SMRT_DEV_DB_URL or cli.database); returning static-only result: ${staticHint}. Static tools are unaffected.`
|
|
1337
|
+
}],
|
|
1338
|
+
data: {
|
|
1339
|
+
provenance: STATIC_PROVENANCE,
|
|
1340
|
+
connected: false
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
const { db, source, displayUrl, databaseType } = resolved;
|
|
1344
|
+
try {
|
|
1345
|
+
const { data, diagnostics } = await read(db);
|
|
1346
|
+
return {
|
|
1347
|
+
ok: true,
|
|
1348
|
+
coverage: null,
|
|
1349
|
+
diagnostics,
|
|
1350
|
+
data: {
|
|
1351
|
+
provenance: RUNTIME_PROVENANCE,
|
|
1352
|
+
connected: true,
|
|
1353
|
+
connectionSource: source,
|
|
1354
|
+
databaseType,
|
|
1355
|
+
displayUrl,
|
|
1356
|
+
...data
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
return {
|
|
1361
|
+
ok: true,
|
|
1362
|
+
coverage: null,
|
|
1363
|
+
diagnostics: [{
|
|
1364
|
+
severity: "warning",
|
|
1365
|
+
code: "runtime_read_error",
|
|
1366
|
+
message: safeErrorMessage(error)
|
|
1367
|
+
}],
|
|
1368
|
+
data: {
|
|
1369
|
+
provenance: RUNTIME_PROVENANCE,
|
|
1370
|
+
connected: true,
|
|
1371
|
+
connectionSource: source,
|
|
1372
|
+
databaseType,
|
|
1373
|
+
displayUrl
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
} finally {
|
|
1377
|
+
await closeRuntimeConnection(db);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Stored error columns (`error_message`, `last_error`) are free text written
|
|
1382
|
+
* at failure time and routinely quote connection URLs or credentials. Every
|
|
1383
|
+
* string in a live result passes through {@link redactConnectionString}
|
|
1384
|
+
* before it reaches an MCP client; structure and non-string values are kept.
|
|
1385
|
+
*/
|
|
1386
|
+
function redactStrings(value) {
|
|
1387
|
+
if (typeof value === "string") return redactConnectionString(value);
|
|
1388
|
+
if (Array.isArray(value)) return value.map((item) => redactStrings(item));
|
|
1389
|
+
if (value !== null && typeof value === "object") {
|
|
1390
|
+
const out = {};
|
|
1391
|
+
for (const [key, item] of Object.entries(value)) out[key] = redactStrings(item);
|
|
1392
|
+
return out;
|
|
1393
|
+
}
|
|
1394
|
+
return value;
|
|
1395
|
+
}
|
|
1396
|
+
/** Convert a reader result into envelope data + diagnostics. */
|
|
1397
|
+
function toEnvelopeParts(rawResult) {
|
|
1398
|
+
const result = redactStrings(rawResult);
|
|
1399
|
+
if (result !== null && typeof result === "object" && "available" in result && result.available === false) {
|
|
1400
|
+
const unavailable = result;
|
|
1401
|
+
const { message, ...rest } = unavailable;
|
|
1402
|
+
return {
|
|
1403
|
+
data: rest,
|
|
1404
|
+
diagnostics: [{
|
|
1405
|
+
severity: unavailable.reason === "retired" ? "info" : "warning",
|
|
1406
|
+
code: `category_unavailable_${String(unavailable.reason).replace(/-/g, "_")}`,
|
|
1407
|
+
message: String(message)
|
|
1408
|
+
}]
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
data: result,
|
|
1413
|
+
diagnostics: []
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function readToParts(read) {
|
|
1417
|
+
return read.then((result) => toEnvelopeParts(result));
|
|
1418
|
+
}
|
|
1419
|
+
async function runtimeMigrationStatus(args = {}) {
|
|
1420
|
+
const { limit, ...connectionArgs } = args;
|
|
1421
|
+
return withRuntimeConnection(connectionArgs, (db) => readToParts(readMigrationStatus(db, { limit })), "no migration status — the manifest still reports the declared schema");
|
|
1422
|
+
}
|
|
1423
|
+
async function runtimeJobHealth(args = {}) {
|
|
1424
|
+
const { limit, ...connectionArgs } = args;
|
|
1425
|
+
return withRuntimeConnection(connectionArgs, (db) => readToParts(readJobHealth(db, { limit })), "no job health snapshot — the manifest still reports declared job queues");
|
|
1426
|
+
}
|
|
1427
|
+
async function runtimeScheduleHealth(args = {}) {
|
|
1428
|
+
const { limit, ...connectionArgs } = args;
|
|
1429
|
+
return withRuntimeConnection(connectionArgs, (db) => readToParts(readScheduleHealth(db, { limit })), "no schedule health snapshot — the manifest still reports declared schedules");
|
|
1430
|
+
}
|
|
1431
|
+
async function runtimeDispatchHealth(args = {}) {
|
|
1432
|
+
const { limit, ...connectionArgs } = args;
|
|
1433
|
+
return withRuntimeConnection(connectionArgs, (db) => readToParts(readDispatchHealth(db, { limit })), "no dispatch health snapshot — the manifest still reports declared dispatch topology");
|
|
1434
|
+
}
|
|
1435
|
+
async function runtimeRecentChanges(args = {}) {
|
|
1436
|
+
const { since, tables, tenantId, limit, ...connectionArgs } = args;
|
|
1437
|
+
return withRuntimeConnection(connectionArgs, (db) => readToParts(readRecentChanges(db, {
|
|
1438
|
+
since,
|
|
1439
|
+
tables,
|
|
1440
|
+
tenantId,
|
|
1441
|
+
limit
|
|
1442
|
+
})), "no recent changes — static knowledge artifacts are unchanged");
|
|
1443
|
+
}
|
|
1444
|
+
async function runtimeRegistryDrift(args = {}) {
|
|
1445
|
+
return withRuntimeConnection(args, (db) => readToParts(readRegistryDrift(db)), "no registry drift report — _smrt_registry is retired; declared objects come from the manifest");
|
|
1446
|
+
}
|
|
1447
|
+
//#endregion
|
|
1055
1448
|
//#region src/index.ts
|
|
1056
1449
|
/**
|
|
1057
1450
|
* SMRT Development MCP Server
|
|
@@ -1329,7 +1722,7 @@ function createServer() {
|
|
|
1329
1722
|
const { name, arguments: args } = request.params;
|
|
1330
1723
|
if (DEBUG) {
|
|
1331
1724
|
console.error(`[${SERVER_NAME}] CallTool: ${name}`);
|
|
1332
|
-
console.error(`[${SERVER_NAME}] Arguments:`, JSON.stringify(args, null, 2));
|
|
1725
|
+
console.error(`[${SERVER_NAME}] Arguments:`, JSON.stringify(redactDebugArguments(args), null, 2));
|
|
1333
1726
|
}
|
|
1334
1727
|
try {
|
|
1335
1728
|
let result;
|
|
@@ -1404,6 +1797,24 @@ function createServer() {
|
|
|
1404
1797
|
case "get-agent-skill":
|
|
1405
1798
|
result = JSON.stringify(await getAgentSkill(args), null, 2);
|
|
1406
1799
|
break;
|
|
1800
|
+
case "migration-status":
|
|
1801
|
+
result = JSON.stringify(await runtimeMigrationStatus(args), null, 2);
|
|
1802
|
+
break;
|
|
1803
|
+
case "job-health":
|
|
1804
|
+
result = JSON.stringify(await runtimeJobHealth(args), null, 2);
|
|
1805
|
+
break;
|
|
1806
|
+
case "schedule-health":
|
|
1807
|
+
result = JSON.stringify(await runtimeScheduleHealth(args), null, 2);
|
|
1808
|
+
break;
|
|
1809
|
+
case "dispatch-health":
|
|
1810
|
+
result = JSON.stringify(await runtimeDispatchHealth(args), null, 2);
|
|
1811
|
+
break;
|
|
1812
|
+
case "recent-changes":
|
|
1813
|
+
result = JSON.stringify(await runtimeRecentChanges(args), null, 2);
|
|
1814
|
+
break;
|
|
1815
|
+
case "registry-drift":
|
|
1816
|
+
result = JSON.stringify(await runtimeRegistryDrift(args), null, 2);
|
|
1817
|
+
break;
|
|
1407
1818
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
1408
1819
|
}
|
|
1409
1820
|
return {
|
|
@@ -1522,11 +1933,26 @@ function detailArg(args) {
|
|
|
1522
1933
|
const detail = args?.detail;
|
|
1523
1934
|
return typeof detail === "string" ? detail : void 0;
|
|
1524
1935
|
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Debug logging happens before any tool runs its own redaction, so a
|
|
1938
|
+
* credential-bearing `dbUrl` argument must be masked here.
|
|
1939
|
+
*/
|
|
1940
|
+
function redactDebugArguments(args) {
|
|
1941
|
+
if (!isRecord(args) || typeof args.dbUrl !== "string") return args;
|
|
1942
|
+
return {
|
|
1943
|
+
...args,
|
|
1944
|
+
dbUrl: redactConnectionString(args.dbUrl)
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function isRuntimeEnvelope(value) {
|
|
1948
|
+
return isRecord(value) && typeof value.ok === "boolean" && "coverage" in value && Array.isArray(value.diagnostics) && "data" in value;
|
|
1949
|
+
}
|
|
1525
1950
|
function toDevToolStructuredContent(result) {
|
|
1526
1951
|
let data = result;
|
|
1527
1952
|
try {
|
|
1528
1953
|
data = JSON.parse(result);
|
|
1529
1954
|
} catch {}
|
|
1955
|
+
if (isRuntimeEnvelope(data)) return data;
|
|
1530
1956
|
const source = isRecord(data) ? data : void 0;
|
|
1531
1957
|
return {
|
|
1532
1958
|
ok: true,
|