@deepsql/mcp 0.13.0 → 0.14.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/AGENT-SETUP.md +15 -6
- package/CLAUDE.md +20 -6
- package/README.md +109 -25
- package/claude_desktop_config.customer.example.json +3 -11
- package/codex_config.customer.example.toml +12 -8
- package/deepsql-phase1-lib.js +225 -2
- package/deepsql-phase1-server.js +1 -1
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +169 -33
- package/src/cli.js +34 -0
- package/src/commands/index-recommendations.js +426 -0
- package/src/commands/index-recommendations.test.js +323 -0
- package/src/commands/login.js +46 -7
- package/src/commands/login.test.js +111 -0
- package/src/commands/mcp.js +162 -10
- package/src/commands/mcp.test.js +145 -0
package/AGENT-SETUP.md
CHANGED
|
@@ -193,7 +193,8 @@ can switch later with another `connections use`.
|
|
|
193
193
|
Pick the user's editor and run one of:
|
|
194
194
|
|
|
195
195
|
```bash
|
|
196
|
-
deepsql mcp config --install --for claude-code # MCP:
|
|
196
|
+
deepsql mcp config --install --for claude-code # MCP: `claude mcp add --scope user`
|
|
197
|
+
# (falls back to ~/.claude.json if claude CLI missing)
|
|
197
198
|
# Skill: ~/.claude/skills/deepsql/SKILL.md
|
|
198
199
|
deepsql mcp config --install --for claude-desktop # MCP: ~/Library/Application Support/Claude/...
|
|
199
200
|
# (no skill — Claude Desktop has no skills surface)
|
|
@@ -227,11 +228,19 @@ Each invocation does **two** things in one shot:
|
|
|
227
228
|
|
|
228
229
|
The installer:
|
|
229
230
|
|
|
230
|
-
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
- For **Claude Code**, delegates to `claude mcp add --scope user deepsql deepsql mcp`
|
|
232
|
+
(the official CLI that handles user-vs-project-vs-local scope correctly).
|
|
233
|
+
Falls back to writing `~/.claude.json` directly if the `claude` CLI isn't
|
|
234
|
+
on PATH (e.g. SSH boxes, CI). The older installer wrote to
|
|
235
|
+
`~/.claude/settings.json` which is the wrong file — that's for
|
|
236
|
+
permissions/hooks, not MCP. If a stale entry exists from a manual setup
|
|
237
|
+
or older installer, use `--force` to remove + re-add it.
|
|
238
|
+
- For the other editors, creates each target file + parent directory if
|
|
239
|
+
missing, merges into the existing MCP server list without touching
|
|
240
|
+
siblings, refuses to overwrite an existing `deepsql` entry unless
|
|
241
|
+
`--force` is set, and backs up the existing file to
|
|
242
|
+
`<path>.bak.<timestamp>` before any change.
|
|
243
|
+
- For Codex's `AGENTS.md`, wraps the skill in guarded
|
|
235
244
|
`<!-- BEGIN DEEPSQL ... -->` / `<!-- END DEEPSQL ... -->` markers and
|
|
236
245
|
preserves the user's surrounding content,
|
|
237
246
|
- emits the destination path and any backup path so the user can revert.
|
package/CLAUDE.md
CHANGED
|
@@ -33,8 +33,9 @@ server, and the statement you ran.** Don't be sloppy.
|
|
|
33
33
|
|
|
34
34
|
## The tools you have
|
|
35
35
|
|
|
36
|
-
The MCP server exposes
|
|
37
|
-
returned by `list_connections`)
|
|
36
|
+
The MCP server exposes 12 tools. They all take a `connectionId` (UUID
|
|
37
|
+
returned by `list_connections`) except `apply_index_recommendation`,
|
|
38
|
+
which takes a server-resolved `recommendationId`.
|
|
38
39
|
|
|
39
40
|
| Tool | Purpose |
|
|
40
41
|
|---|---|
|
|
@@ -46,6 +47,8 @@ returned by `list_connections`).
|
|
|
46
47
|
| `get_relationships` | Inferred + validated foreign keys with confidence scores. Many real DBs lack declared FKs; this fills the gap. |
|
|
47
48
|
| `get_anti_patterns` | Schema-level (`kind=table`) or query-level (`kind=query`) anti-patterns. |
|
|
48
49
|
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples. Read-only; doesn't trigger new work. |
|
|
50
|
+
| `get_index_recommendations` | **Workload-weighted DBA-grade index advisor.** Pre-computed top-N (default 5) recommendations ranked by net benefit (`Σ calls × mean_exec_time` − write-cost). Each result carries up to 5 contributing query fingerprints, the role each column played, and optional HypoPG cost-delta on Postgres. Covers both `CREATE_INDEX` and `DROP_INDEX` (unused + redundant-prefix) candidates. |
|
|
51
|
+
| **`apply_index_recommendation`** | **The only write-capable MCP tool.** Apply (or dry-run) a recommendation against its target connection and measure the before/after benefit on contributing queries. `DRY_RUN` (default) uses HypoPG (Postgres-only) for zero-write cost-delta. `APPLY` runs real `CREATE/DROP INDEX CONCURRENTLY` (configurable via `concurrent`). `APPLY_AND_MEASURE` additionally runs `EXPLAIN ANALYZE` for wall-clock timings. Write modes require `confirm: true`. The DDL is server-generated from the recommendation row — clients never supply SQL. |
|
|
49
52
|
| **`execute_sql`** | **Run any SQL statement.** Policy is server-enforced: developers can run SELECT/WITH/SHOW/EXPLAIN; admins can also run DML/DDL with a two-step confirmation. EXPLAIN and EXPLAIN ANALYZE are just SQL — no separate flag. |
|
|
50
53
|
| **`analyze_query_plan`** | **AI-enriched plan analysis** for a query. Returns the parsed plan tree, performance issues, index recommendations, and a written summary that takes the connection's schema and business rules into account. Pass `useAnalyze: true` to run `EXPLAIN ANALYZE` (actually executes the query). |
|
|
51
54
|
|
|
@@ -206,9 +209,19 @@ gates that protect `execute_sql` kick in. You'll get back
|
|
|
206
209
|
get `requiresConfirmation` — surface the warnings to the user verbatim, wait
|
|
207
210
|
for them to say yes, then re-call with `confirmMutation: true`.
|
|
208
211
|
|
|
209
|
-
**"What indexes should we add?"** →
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
**"What indexes should we add?"** → `get_index_recommendations`. Returns
|
|
213
|
+
the workload-weighted top-N (default 5) with net benefit, contributing
|
|
214
|
+
queries, and HypoPG cost-delta when available. Use `deepsql indexes
|
|
215
|
+
missing` / `deepsql indexes health` in the terminal for catalog-level
|
|
216
|
+
counterparts (usage stats, duplicates).
|
|
217
|
+
|
|
218
|
+
**"How much faster will this index actually make things?"** →
|
|
219
|
+
`apply_index_recommendation` with `mode: "DRY_RUN"` (default). On
|
|
220
|
+
Postgres it uses HypoPG to install a virtual index, EXPLAINs each
|
|
221
|
+
contributing query, and reports the planner cost delta — no writes
|
|
222
|
+
hit the database. If the user wants real timings, pass
|
|
223
|
+
`mode: "APPLY_AND_MEASURE"` plus `confirm: true` to actually create
|
|
224
|
+
the index (CONCURRENTLY) and run `EXPLAIN ANALYZE` before/after.
|
|
212
225
|
|
|
213
226
|
**"What changed recently / what should I worry about?"** → Tell the user to
|
|
214
227
|
run `deepsql digest` (today) or `deepsql digest 7` (last seven). The digest
|
|
@@ -305,10 +318,11 @@ them at the terminal command rather than trying to fake it through
|
|
|
305
318
|
|
|
306
319
|
| Capability | CLI command |
|
|
307
320
|
|---|---|
|
|
308
|
-
|
|
|
321
|
+
| Catalog-level index stats (counterpart to `get_index_recommendations`) | `deepsql indexes list`, `deepsql indexes missing` |
|
|
309
322
|
| Unused / duplicate index detection | `deepsql indexes unused`, `deepsql indexes duplicates` |
|
|
310
323
|
| Per-table index usage stats | `deepsql indexes usage <table>` |
|
|
311
324
|
| Index health report | `deepsql indexes health` |
|
|
325
|
+
| Workload-weighted advisor + apply (DBA-grade) | `deepsql index-recommendations top` / `apply <id>` — same surface as the MCP `get_index_recommendations` + `apply_index_recommendation` tools, useful from a terminal |
|
|
312
326
|
| Daily digest (anomalies + AI commentary) | `deepsql digest`, `deepsql digest 7` |
|
|
313
327
|
| Streaming AI optimization for a slow query | `deepsql slow-queries optimize --query-id <id>` |
|
|
314
328
|
|
package/README.md
CHANGED
|
@@ -1,42 +1,126 @@
|
|
|
1
|
-
# DeepSQL MCP
|
|
1
|
+
# DeepSQL CLI + MCP server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `@deepsql/mcp` package ships two things in one binary:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **`deepsql`** — a CLI for talking to a self-hosted DeepSQL backend from a
|
|
6
|
+
terminal (auth, connections, SQL execution, plan analysis, index
|
|
7
|
+
suggestions, slow-query analyses, admin ops).
|
|
8
|
+
- **`deepsql mcp`** — a stdio MCP server that exposes the same backend to
|
|
9
|
+
editor agents (Claude Code, Cursor, Codex, Claude Desktop) so they can
|
|
10
|
+
query and reason about the user's databases.
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- Read-only SQL execution and explain through backend-enforced MCP endpoints
|
|
12
|
+
Both share one auth file (`~/.config/deepsql/auth.json`, mode 0600). Log
|
|
13
|
+
in once with `deepsql login`; the MCP server uses the same token
|
|
14
|
+
automatically — no token needs to be embedded in your editor's config.
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
## Install
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
| `DEEPSQL_API_BASE_URL` | yes | `https://customer-deepsql.example.com/api/` |
|
|
19
|
-
| `DEEPSQL_AUTH_TOKEN` | yes | `dsql_mcp_...` |
|
|
20
|
-
| `DEEPSQL_MCP_TIMEOUT_MS` | no | `120000` |
|
|
21
|
-
| `DEEPSQL_MCP_USER_ID` | no | `codex-mcp` |
|
|
22
|
-
| `DEEPSQL_MCP_PROJECT_ID` | no | `codex-mcp` |
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @deepsql/mcp@latest
|
|
20
|
+
deepsql --version
|
|
21
|
+
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Requires Node ≥ 20.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
## Quick start
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
```bash
|
|
28
|
+
deepsql login --url https://your-deepsql-host.example.com
|
|
29
|
+
deepsql connections list
|
|
30
|
+
deepsql connections use <connection-name>
|
|
31
|
+
deepsql query "SELECT 1 AS ok"
|
|
32
|
+
```
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
## Wire into your editor (one command per editor)
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
The installer writes the MCP server entry into the editor's config AND
|
|
37
|
+
installs a "DBA consult" skill that auto-triggers when the user asks
|
|
38
|
+
the agent to do database work:
|
|
33
39
|
|
|
34
40
|
```bash
|
|
35
|
-
|
|
41
|
+
deepsql mcp config --install --for claude-code # via `claude mcp add --scope user`
|
|
42
|
+
deepsql mcp config --install --for claude-desktop # macOS ~/Library/.../Claude/...
|
|
43
|
+
deepsql mcp config --install --for cursor # ~/.cursor/mcp.json + ~/.cursor/rules/deepsql.mdc
|
|
44
|
+
deepsql mcp config --install --for codex # ~/.codex/config.toml + ~/.codex/AGENTS.md
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
Pass `--print` to see what would be written without touching disk.
|
|
48
|
+
Pass `--no-skill` to install only the MCP entry. Pass `--force` to
|
|
49
|
+
overwrite a stale entry. See `deepsql mcp --help` for details.
|
|
50
|
+
|
|
51
|
+
Restart the editor for the entry to load.
|
|
52
|
+
|
|
53
|
+
## What the MCP server exposes
|
|
54
|
+
|
|
55
|
+
10 tools, all read-only at the schema/retrieval layer and policy-gated
|
|
56
|
+
at the SQL layer:
|
|
57
|
+
|
|
58
|
+
| Tool | Purpose |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `list_connections` | Connections this token has access to |
|
|
61
|
+
| `get_schema` | Cached schema metadata (tables, columns, FKs, types) |
|
|
62
|
+
| `get_database_objects` | Tables, views, functions, procedures |
|
|
63
|
+
| `get_brain_context` | Retrieval brain: tables/columns/FKs/training docs/rules for a question |
|
|
64
|
+
| `list_business_rules` | Active business rules and SQL guardrails for a connection |
|
|
65
|
+
| `get_relationships` | Inferred + validated foreign keys with confidence scores |
|
|
66
|
+
| `get_anti_patterns` | Schema-level or query-level anti-patterns |
|
|
67
|
+
| `analyze_slow_queries` | Recent slow queries with fingerprints, durations, examples |
|
|
68
|
+
| `execute_sql` | Run any SQL — backend enforces role-based policy (developers read-only, admins can mutate with two-step confirm) |
|
|
69
|
+
| `analyze_query_plan` | AI-enriched plan analysis (parsed plan tree, performance issues, index recommendations, written summary that uses the connection's schema + business rules) |
|
|
70
|
+
|
|
71
|
+
EXPLAIN and EXPLAIN ANALYZE are just SQL — pass them as the query to
|
|
72
|
+
`execute_sql`. For the AI-enriched plan analysis with the LLM-written
|
|
73
|
+
summary, use `analyze_query_plan`.
|
|
74
|
+
|
|
75
|
+
## Runtime guidance for agents
|
|
76
|
+
|
|
77
|
+
`CLAUDE.md` (bundled in this package, at
|
|
78
|
+
`node_modules/@deepsql/mcp/CLAUDE.md` after install) is the runtime
|
|
79
|
+
guide for editor agents that have these tools loaded. It covers the
|
|
80
|
+
"DBA consult" pattern, decision tree, hard rules around the role-gated
|
|
81
|
+
mutation flow, and common foot-guns. The installer also drops a
|
|
82
|
+
shortened, trigger-focused version of that guide as a native skill so
|
|
83
|
+
the pattern fires automatically on phrases like "add a table", "write
|
|
84
|
+
a migration", "design a schema", "query the database".
|
|
85
|
+
|
|
86
|
+
## Agent-driven setup (one paste)
|
|
87
|
+
|
|
88
|
+
For a fresh customer install, paste `AGENT-SETUP.md` (bundled at
|
|
89
|
+
`node_modules/@deepsql/mcp/AGENT-SETUP.md`) into Claude Code / Cursor /
|
|
90
|
+
Codex. The agent walks the user through install, login, connection
|
|
91
|
+
registration, editor integration, and end-to-end validation in ~5
|
|
92
|
+
minutes.
|
|
93
|
+
|
|
94
|
+
## Manual install (only if you don't want the CLI shim)
|
|
95
|
+
|
|
96
|
+
If you'd rather skip the `deepsql mcp config --install` flow and wire
|
|
97
|
+
the editor config by hand, see the example files bundled with this
|
|
98
|
+
package: `claude_desktop_config.customer.example.json` and
|
|
99
|
+
`codex_config.customer.example.toml`. The token-embedded shape in
|
|
100
|
+
those examples still works, but `deepsql mcp config --install` is the
|
|
101
|
+
recommended path now.
|
|
102
|
+
|
|
103
|
+
## Run the server directly (advanced)
|
|
39
104
|
|
|
40
105
|
```bash
|
|
41
|
-
|
|
106
|
+
deepsql mcp # uses the saved profile + auth token
|
|
107
|
+
npx -y @deepsql/mcp # one-off invocation without install
|
|
42
108
|
```
|
|
109
|
+
|
|
110
|
+
Both work; the CLI shim is preferred because it shares the saved
|
|
111
|
+
profile and never asks you to paste a token into editor config.
|
|
112
|
+
|
|
113
|
+
## Environment variables
|
|
114
|
+
|
|
115
|
+
| Variable | Required | Used by | Example |
|
|
116
|
+
|----------|----------|---------|---------|
|
|
117
|
+
| `DEEPSQL_API_BASE_URL` | for npx-style invocation | `deepsql mcp` if no saved profile | `https://customer-deepsql.example.com/api/` |
|
|
118
|
+
| `DEEPSQL_AUTH_TOKEN` | for npx-style invocation | bearer for backend; `deepsql login` writes one to the profile file | `dsql_mcp_…` |
|
|
119
|
+
| `DEEPSQL_MCP_USER_ID` | no | identifies the editor that invoked the MCP server in audit logs | `claude-desktop` / `cursor-mcp` / `codex-mcp` |
|
|
120
|
+
| `DEEPSQL_CALLER_AGENT` | no | overrides the CLI's audit identity when an agent shells out to `deepsql` | `claude-code` |
|
|
121
|
+
| `DEEPSQL_MCP_TIMEOUT_MS` | no | per-request timeout for MCP server calls | `120000` |
|
|
122
|
+
|
|
123
|
+
If `deepsql login` has been run, both the CLI and the spawned MCP
|
|
124
|
+
server pick up `DEEPSQL_API_BASE_URL` + `DEEPSQL_AUTH_TOKEN` from
|
|
125
|
+
`~/.config/deepsql/auth.json` automatically — you don't need to set
|
|
126
|
+
the env vars for either.
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"_comment": "Manual Claude Desktop config example. Most users should run `deepsql mcp config --install --for claude-desktop` instead — that handles the OS-specific path, backup, and the DBA-consult skill in one shot. Use this template only if you need a custom layout (per-workspace, embedded token, etc.).",
|
|
2
3
|
"mcpServers": {
|
|
3
4
|
"deepsql": {
|
|
4
|
-
"command": "
|
|
5
|
-
"args": [
|
|
6
|
-
"-y",
|
|
7
|
-
"@deepsql/mcp"
|
|
8
|
-
],
|
|
9
|
-
"env": {
|
|
10
|
-
"DEEPSQL_API_BASE_URL": "https://customer-deepsql.example.com/api/",
|
|
11
|
-
"DEEPSQL_AUTH_TOKEN": "dsql_mcp_replace_me",
|
|
12
|
-
"DEEPSQL_MCP_USER_ID": "claude-desktop",
|
|
13
|
-
"DEEPSQL_MCP_PROJECT_ID": "claude-desktop"
|
|
14
|
-
}
|
|
5
|
+
"command": "deepsql",
|
|
6
|
+
"args": ["mcp"]
|
|
15
7
|
}
|
|
16
8
|
}
|
|
17
9
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Manual Codex CLI MCP config example. Most users should run
|
|
2
|
+
# `deepsql mcp config --install --for codex`
|
|
3
|
+
# instead — that handles the file path, backup, and the DBA-consult skill
|
|
4
|
+
# in one shot. Use this template only if you need a custom layout (e.g.
|
|
5
|
+
# embedded token, alternate `DEEPSQL_*` env vars, non-default profile).
|
|
6
|
+
#
|
|
7
|
+
# The form below uses the `deepsql mcp` shim, which reads auth from
|
|
8
|
+
# ~/.config/deepsql/auth.json (written by `deepsql login`) — no token
|
|
9
|
+
# needs to be embedded in the config.
|
|
4
10
|
|
|
5
|
-
[mcp_servers.deepsql
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
DEEPSQL_MCP_USER_ID = "codex-mcp"
|
|
9
|
-
DEEPSQL_MCP_PROJECT_ID = "codex-mcp"
|
|
11
|
+
[mcp_servers.deepsql]
|
|
12
|
+
command = "deepsql"
|
|
13
|
+
args = ["mcp"]
|
package/deepsql-phase1-lib.js
CHANGED
|
@@ -143,6 +143,56 @@ const TOOL_DEFINITIONS = [
|
|
|
143
143
|
additionalProperties: false,
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: "apply_index_recommendation",
|
|
148
|
+
description:
|
|
149
|
+
"Apply (or dry-run) an index recommendation and measure the before/after benefit on the queries that motivated it. " +
|
|
150
|
+
"Default mode is DRY_RUN — no writes; uses HypoPG (Postgres-only) to install a virtual index in the session, EXPLAIN-diffs the cost on each contributing query, then resets. " +
|
|
151
|
+
"APPLY mode runs the real DDL (CREATE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY on Postgres so the operation doesn't lock the table). " +
|
|
152
|
+
"APPLY_AND_MEASURE additionally runs EXPLAIN ANALYZE before and after for wall-clock timings — slowest, only opt in when you're OK executing the contributing queries against the target DB. " +
|
|
153
|
+
"Both APPLY modes require `confirm: true` — write operations don't happen by accident. " +
|
|
154
|
+
"Returns the executed DDL, the planner-cost delta, per-sample measurements (each contributing query's before/after cost), and an aggregate improvement percentage.",
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
recommendationId: { type: "string", description: "Recommendation row id (from get_index_recommendations)." },
|
|
159
|
+
mode: {
|
|
160
|
+
type: "string",
|
|
161
|
+
enum: ["DRY_RUN", "APPLY", "APPLY_AND_MEASURE"],
|
|
162
|
+
description: "Default DRY_RUN. APPLY mutates the database; APPLY_AND_MEASURE also runs EXPLAIN ANALYZE."
|
|
163
|
+
},
|
|
164
|
+
confirm: {
|
|
165
|
+
type: "boolean",
|
|
166
|
+
description: "Required `true` for APPLY and APPLY_AND_MEASURE. Defaults to false."
|
|
167
|
+
},
|
|
168
|
+
concurrent: {
|
|
169
|
+
type: "boolean",
|
|
170
|
+
description: "Postgres-only. When true (default), CREATE/DROP runs CONCURRENTLY (no table lock, but waits for every pre-existing transaction). Set false on small dev tables when the brief ACCESS EXCLUSIVE lock is acceptable."
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: ["recommendationId"],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "get_index_recommendations",
|
|
179
|
+
description:
|
|
180
|
+
"Get DeepSQL's pre-computed top index recommendations for a connection. Recommendations are workload-weighted (Σ calls × mean_exec_time, the pganalyze / Microsoft DTA 'total time' ROI metric) and aggregated across many slow-query log fetches over a configurable lookback (default 30 days). Column ordering for composite indexes follows industry rules (equality before range, selectivity-ranked, ORDER BY suffix only with full-equality prefix, capped at 3 columns). Covers both CREATE_INDEX and DROP_INDEX (unused + redundant prefix-duplicate) candidates. Each result carries net benefit (workload − write cost), the contributing query fingerprints, and call/duration metrics — so a caller can audit *why* each suggestion exists rather than trust a heuristic. Returns top N (default 5) PENDING recommendations.",
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
connectionId: { type: "string", description: "DeepSQL connection ID." },
|
|
185
|
+
limit: {
|
|
186
|
+
type: "integer",
|
|
187
|
+
minimum: 1,
|
|
188
|
+
maximum: 50,
|
|
189
|
+
description: "Number of recommendations to return. Defaults to 5.",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
required: ["connectionId"],
|
|
193
|
+
additionalProperties: false,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
146
196
|
{
|
|
147
197
|
name: "analyze_slow_queries",
|
|
148
198
|
description:
|
|
@@ -551,12 +601,143 @@ function summarizeAntiPatterns(payload, kind) {
|
|
|
551
601
|
return `${list.length} query anti-pattern(s)${sevStr ? ` (${sevStr})` : ""}.`;
|
|
552
602
|
}
|
|
553
603
|
|
|
604
|
+
function formatMillisHuman(ms) {
|
|
605
|
+
if (ms == null || !Number.isFinite(ms) || ms <= 0) return null;
|
|
606
|
+
if (ms >= 86_400_000) return `${(ms / 86_400_000).toFixed(1)}d`;
|
|
607
|
+
if (ms >= 3_600_000) return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
608
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
609
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
610
|
+
return `${ms}ms`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function summarizeApplyResult(payload) {
|
|
614
|
+
if (!payload || typeof payload !== "object") {
|
|
615
|
+
return "Apply call returned no body.";
|
|
616
|
+
}
|
|
617
|
+
const status = payload.status || "?";
|
|
618
|
+
const mode = payload.mode || "?";
|
|
619
|
+
if (status === "BLOCKED_NEEDS_CONFIRMATION") {
|
|
620
|
+
return `[${mode}] blocked — pass confirm=true to mutate the database.`;
|
|
621
|
+
}
|
|
622
|
+
if (status === "NOT_FOUND") {
|
|
623
|
+
return `[${mode}] recommendation not found: ${payload.recommendationId || "?"}`;
|
|
624
|
+
}
|
|
625
|
+
if (status === "NO_USABLE_SAMPLES") {
|
|
626
|
+
return `[${mode}] no literal-bearing contributing queries available; cannot measure.`;
|
|
627
|
+
}
|
|
628
|
+
if (status === "FAILED") {
|
|
629
|
+
return `[${mode}] failed: ${payload.message || "(no message)"}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const lines = [];
|
|
633
|
+
lines.push(`[${mode}] ${status} — ${payload.executedDdl || "(no ddl)"}`);
|
|
634
|
+
if (payload.beforeCost != null && payload.afterCost != null) {
|
|
635
|
+
const pct = payload.costReductionPct;
|
|
636
|
+
lines.push(
|
|
637
|
+
` planner cost: ${payload.beforeCost.toFixed(0)} → ${payload.afterCost.toFixed(0)}` +
|
|
638
|
+
(pct != null ? ` (${pct >= 0 ? "−" : "+"}${Math.abs(pct).toFixed(1)}%)` : "")
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (payload.beforeWallTimeMs != null && payload.afterWallTimeMs != null) {
|
|
642
|
+
const pct = payload.wallTimeImprovementPct;
|
|
643
|
+
lines.push(
|
|
644
|
+
` wall time: ${payload.beforeWallTimeMs.toFixed(1)}ms → ${payload.afterWallTimeMs.toFixed(1)}ms` +
|
|
645
|
+
(pct != null ? ` (${pct >= 0 ? "−" : "+"}${Math.abs(pct).toFixed(1)}%)` : "")
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (Array.isArray(payload.samples) && payload.samples.length) {
|
|
649
|
+
lines.push(` ${payload.samples.length} contributing query sample(s):`);
|
|
650
|
+
for (const s of payload.samples.slice(0, 5)) {
|
|
651
|
+
const before = s.beforeCost != null ? s.beforeCost.toFixed(0) : "?";
|
|
652
|
+
const after = s.afterCost != null ? s.afterCost.toFixed(0) : "?";
|
|
653
|
+
lines.push(
|
|
654
|
+
` fp=${(s.fingerprint || "?").slice(0, 12)} cost ${before} → ${after}` +
|
|
655
|
+
(s.error ? ` (error: ${s.error})` : "")
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return lines.join("\n");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function summarizeIndexRecommendations(payload) {
|
|
663
|
+
const list = Array.isArray(payload) ? payload : payload?.recommendations || [];
|
|
664
|
+
if (!list.length) {
|
|
665
|
+
return "No pending index recommendations. The scheduler may not have run yet, or the workload has none worth flagging.";
|
|
666
|
+
}
|
|
667
|
+
const lines = list.slice(0, 10).map((rec, idx) => {
|
|
668
|
+
const table = rec.tableName || "?";
|
|
669
|
+
const prio = rec.priority || "?";
|
|
670
|
+
const occ = rec.occurrenceCount != null ? `seen ${rec.occurrenceCount}×` : "";
|
|
671
|
+
const net = formatMillisHuman(rec.netBenefitMs);
|
|
672
|
+
const writeCost = formatMillisHuman(rec.writeCostScore);
|
|
673
|
+
const evidence = rec.evidenceCount != null && rec.evidenceCount > 0
|
|
674
|
+
? `${rec.evidenceCount} ev` : "";
|
|
675
|
+
const isDrop = rec.kind === "DROP_INDEX";
|
|
676
|
+
const action = isDrop ? "DROP" : "CREATE";
|
|
677
|
+
const target = isDrop
|
|
678
|
+
? `${table}.${rec.indexName || "?"} (unused)`
|
|
679
|
+
: `${table}(${rec.columnNames || "?"})`;
|
|
680
|
+
// Net benefit is the DBA-grade signal — surface it prominently.
|
|
681
|
+
const benefitClause = net
|
|
682
|
+
? `net=${net} saved` + (writeCost ? `, write=${writeCost}` : "")
|
|
683
|
+
: (rec.estimatedImpact != null ? `impact ${rec.estimatedImpact}` : "");
|
|
684
|
+
const meta = [prio, occ, benefitClause, evidence].filter(Boolean).join(", ");
|
|
685
|
+
return `${idx + 1}. [${action}] ${target}${meta ? ` — ${meta}` : ""}`;
|
|
686
|
+
});
|
|
687
|
+
return `Top ${list.length} pending index recommendation(s):\n${lines.join("\n")}`;
|
|
688
|
+
}
|
|
689
|
+
|
|
554
690
|
function summarizeSlowQueries(payload) {
|
|
555
|
-
|
|
691
|
+
// Backend returns SlowQueryAnalysis with `topSlowQueries` (the field name
|
|
692
|
+
// varies; tolerate both `queries` and `topSlowQueries`).
|
|
693
|
+
const list = Array.isArray(payload?.topSlowQueries)
|
|
694
|
+
? payload.topSlowQueries
|
|
695
|
+
: Array.isArray(payload?.queries) ? payload.queries : [];
|
|
556
696
|
const total = payload?.totalCount ?? list.length;
|
|
557
697
|
const avg = payload?.avgDurationMs;
|
|
558
698
|
const max = payload?.maxDurationMs;
|
|
559
|
-
|
|
699
|
+
|
|
700
|
+
// Three counts matter to a calling agent that's about to EXPLAIN one of
|
|
701
|
+
// these queries:
|
|
702
|
+
//
|
|
703
|
+
// recovered = sourceTruncated AND queryTextRecoveredFromLogs
|
|
704
|
+
// → the live stats source truncated this query, but DeepSQL recovered
|
|
705
|
+
// the full SQL from previously-ingested slow-log data in
|
|
706
|
+
// query_lineage. EXPLAIN will work.
|
|
707
|
+
//
|
|
708
|
+
// stillTruncated = sourceTruncated AND NOT queryTextRecoveredFromLogs
|
|
709
|
+
// → still truncated; EXPLAIN will fail or return a partial plan.
|
|
710
|
+
//
|
|
711
|
+
// neither → normal full-text query, no warning needed.
|
|
712
|
+
const recovered = list.filter((q) =>
|
|
713
|
+
q && q.sourceTruncated === true && q.queryTextRecoveredFromLogs === true
|
|
714
|
+
).length;
|
|
715
|
+
const stillTruncated = list.filter((q) =>
|
|
716
|
+
q && q.sourceTruncated === true && q.queryTextRecoveredFromLogs !== true
|
|
717
|
+
).length;
|
|
718
|
+
|
|
719
|
+
const parts = [];
|
|
720
|
+
if (recovered > 0) {
|
|
721
|
+
parts.push(
|
|
722
|
+
` ℹ ${recovered} ${recovered === 1 ? "query was" : "queries were"} truncated by `
|
|
723
|
+
+ `the database server (default 1024B) but DeepSQL recovered the full SQL `
|
|
724
|
+
+ `from previously-ingested slow-log data — EXPLAIN against \`queryText\` will work.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (stillTruncated > 0) {
|
|
728
|
+
parts.push(
|
|
729
|
+
` ⚠ ${stillTruncated} ${stillTruncated === 1 ? "query is" : "queries are"} still `
|
|
730
|
+
+ `truncated and DeepSQL has no full-text copy on file. EXPLAIN will be unreliable. `
|
|
731
|
+
+ `Fix: ingest the slow query log file for this connection, OR raise `
|
|
732
|
+
+ `\`pg_stat_statements.track_activity_query_size\` (PG) / `
|
|
733
|
+
+ `\`performance_schema_max_sql_text_length\` (MySQL) and restart, then re-collect.`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return `${total} slow query/queries`
|
|
738
|
+
+ `${avg != null ? `, avg=${avg}ms` : ""}`
|
|
739
|
+
+ `${max != null ? `, max=${max}ms` : ""}.`
|
|
740
|
+
+ parts.join("");
|
|
560
741
|
}
|
|
561
742
|
|
|
562
743
|
function summarizeQueryResult(payload) {
|
|
@@ -604,6 +785,12 @@ function buildToolResult(name, payload, extra = {}) {
|
|
|
604
785
|
case "analyze_slow_queries":
|
|
605
786
|
summary = summarizeSlowQueries(payload);
|
|
606
787
|
break;
|
|
788
|
+
case "get_index_recommendations":
|
|
789
|
+
summary = summarizeIndexRecommendations(payload);
|
|
790
|
+
break;
|
|
791
|
+
case "apply_index_recommendation":
|
|
792
|
+
summary = summarizeApplyResult(payload);
|
|
793
|
+
break;
|
|
607
794
|
case "execute_sql":
|
|
608
795
|
summary = summarizeQueryResult(payload);
|
|
609
796
|
break;
|
|
@@ -730,6 +917,40 @@ async function handleToolCall(config, name, args = {}) {
|
|
|
730
917
|
return buildToolResult(name, payload, { kind });
|
|
731
918
|
}
|
|
732
919
|
|
|
920
|
+
case "get_index_recommendations": {
|
|
921
|
+
const connectionId = String(args.connectionId || "").trim();
|
|
922
|
+
if (!connectionId) return buildToolError("connectionId is required.");
|
|
923
|
+
const limit = clampInteger(args.limit, 1, 50, 5);
|
|
924
|
+
const payload = await callDeepSqlApi(
|
|
925
|
+
config,
|
|
926
|
+
`/index-recommendations/${encodeURIComponent(connectionId)}/top?limit=${limit}`,
|
|
927
|
+
);
|
|
928
|
+
return buildToolResult(name, payload);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
case "apply_index_recommendation": {
|
|
932
|
+
const recommendationId = String(args.recommendationId || "").trim();
|
|
933
|
+
if (!recommendationId) return buildToolError("recommendationId is required.");
|
|
934
|
+
const mode = String(args.mode || "DRY_RUN").toUpperCase();
|
|
935
|
+
if (!["DRY_RUN", "APPLY", "APPLY_AND_MEASURE"].includes(mode)) {
|
|
936
|
+
return buildToolError(`Unknown mode: ${mode}. Expected DRY_RUN, APPLY, or APPLY_AND_MEASURE.`);
|
|
937
|
+
}
|
|
938
|
+
const confirm = args.confirm === true;
|
|
939
|
+
if ((mode === "APPLY" || mode === "APPLY_AND_MEASURE") && !confirm) {
|
|
940
|
+
return buildToolError(
|
|
941
|
+
`Mode ${mode} mutates the target database. Re-call with confirm=true to proceed.`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const concurrent = args.concurrent === false ? false : true;
|
|
945
|
+
const qs = `?mode=${encodeURIComponent(mode)}&confirm=${confirm}&concurrent=${concurrent}`;
|
|
946
|
+
const payload = await callDeepSqlApi(
|
|
947
|
+
config,
|
|
948
|
+
`/index-recommendations/${encodeURIComponent(recommendationId)}/apply${qs}`,
|
|
949
|
+
{ method: "POST" },
|
|
950
|
+
);
|
|
951
|
+
return buildToolResult(name, payload);
|
|
952
|
+
}
|
|
953
|
+
|
|
733
954
|
case "analyze_slow_queries": {
|
|
734
955
|
const connectionId = String(args.connectionId || "").trim();
|
|
735
956
|
if (!connectionId) return buildToolError("connectionId is required.");
|
|
@@ -853,5 +1074,7 @@ module.exports = {
|
|
|
853
1074
|
stripTrailingSemicolons,
|
|
854
1075
|
stripSqlComments,
|
|
855
1076
|
stripSqlStringLiterals,
|
|
1077
|
+
summarizeApplyResult,
|
|
1078
|
+
summarizeIndexRecommendations,
|
|
856
1079
|
validateReadOnlySql,
|
|
857
1080
|
};
|
package/deepsql-phase1-server.js
CHANGED
|
@@ -193,7 +193,7 @@ class DeepSqlPhase1McpServer {
|
|
|
193
193
|
},
|
|
194
194
|
serverInfo: SERVER_INFO,
|
|
195
195
|
instructions:
|
|
196
|
-
"DeepSQL
|
|
196
|
+
"DeepSQL MCP exposes the user's database catalogs plus DeepSQL's retrieval brain (relevant tables/columns/FKs, business rules, inferred relationships, anti-patterns, slow-query analysis). Workflow: call list_connections first to get UUIDs; call get_brain_context with the user's question to ground generation in retrieved schema; call execute_sql to run the query (admins can run DDL/DML with a two-step confirmMutation flow, developers are server-enforced read-only); call analyze_query_plan for AI-enriched plan analysis with the connection's schema + business rules in scope. EXPLAIN and EXPLAIN ANALYZE are valid SQL — pass them as the query to execute_sql; use analyze_query_plan when you want the LLM-written summary, not raw plan rows. Always pass connectionId (UUID), not connection names.",
|
|
197
197
|
});
|
|
198
198
|
return;
|
|
199
199
|
}
|