@deepsql/mcp 0.18.2 → 0.20.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.
@@ -350,9 +350,11 @@ const TOOL_DEFINITIONS = [
350
350
  {
351
351
  name: "optimize_slow_query",
352
352
  description:
353
- "Get AI-generated optimization recommendations for a specific slow query — including "
354
- + "index suggestions, query rewrites, and expected performance impact. DeepSQL inspects "
355
- + "the query against the connection's schema, existing indexes, and workload patterns. "
353
+ "Get an AI query REWRITE and plan diagnosis for one specific slow query. "
354
+ + "Single-query scoped: returns a rewritten SQL (validated against the live DB), "
355
+ + "the plan bottleneck, and an estimated improvement. Does NOT recommend indexes "
356
+ + "index and pre-aggregation recommendations require the whole workload and come "
357
+ + "from the holistic Workload Analysis (and the get_index_recommendations tool). "
356
358
  + "Pass `avgExecutionTimeMs` to anchor the impact estimate to a real baseline.",
357
359
  inputSchema: {
358
360
  type: "object",
@@ -518,6 +520,352 @@ const TOOL_DEFINITIONS = [
518
520
  additionalProperties: false,
519
521
  },
520
522
  },
523
+
524
+ // ─── Phase A symmetry: tools added to match the `deepsql` CLI surface ───
525
+ //
526
+ // Each tool below mirrors a CLI subcommand that previously had no MCP
527
+ // equivalent. Connection write operations (add/update/remove) are
528
+ // intentionally NOT exposed as MCP tools — they require DB credentials
529
+ // and we don't want secrets crossing the agent's conversation history.
530
+ // Customers manage connections via `deepsql connections add` at a TTY.
531
+
532
+ {
533
+ name: "get_current_user",
534
+ description:
535
+ "Return the authenticated user behind the current MCP token: username, role, "
536
+ + "and the DeepSQL host this MCP server is bound to. Use this when the agent "
537
+ + "needs to know whether the caller is admin-capable before suggesting a "
538
+ + "DDL/DML run, or when explaining role-based restrictions to the user.",
539
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
540
+ },
541
+ {
542
+ name: "test_connection",
543
+ description:
544
+ "Validate a saved connection: contacts the database, runs the privilege "
545
+ + "report, and returns whether the connection (and SSH tunnel, if any) is "
546
+ + "reachable. Read-only on the customer's DB. Use when diagnosing a `?:` "
547
+ + "Connected status from `list_connections` or before suggesting a SQL run "
548
+ + "against a connection the agent hasn't touched yet. Takes a saved "
549
+ + "connectionId only — does NOT accept ad-hoc credentials (those go through "
550
+ + "`deepsql connections add` at a terminal, not chat).",
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
555
+ },
556
+ required: ["connectionId"],
557
+ additionalProperties: false,
558
+ },
559
+ },
560
+ {
561
+ name: "show_connection",
562
+ description:
563
+ "Return the full saved configuration for one connection with all secret "
564
+ + "fields masked as `(set)`. Useful for diagnosing connection issues "
565
+ + "(host/port/SSL/SSH config). Will never echo a password back; use "
566
+ + "`deepsql connections show` at a TTY if you genuinely need to see "
567
+ + "a secret value.",
568
+ inputSchema: {
569
+ type: "object",
570
+ properties: {
571
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
572
+ },
573
+ required: ["connectionId"],
574
+ additionalProperties: false,
575
+ },
576
+ },
577
+ {
578
+ name: "reinit_connection_brain",
579
+ description:
580
+ "Trigger a fresh brain initialization for a connection: re-scans the "
581
+ + "schema, re-runs key-column / FK inference, re-embeds training context. "
582
+ + "Use after the user reports DeepSQL's schema knowledge is stale (e.g., "
583
+ + "they just ran a migration). Returns immediately with an init-status "
584
+ + "row; the actual reinit runs in the background — poll connection state "
585
+ + "via `list_connections` to see when it transitions back to COMPLETED.",
586
+ inputSchema: {
587
+ type: "object",
588
+ properties: {
589
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
590
+ force: {
591
+ type: "boolean",
592
+ description: "Restart even if a previous init is currently RUNNING. Defaults to false.",
593
+ },
594
+ },
595
+ required: ["connectionId"],
596
+ additionalProperties: false,
597
+ },
598
+ },
599
+ {
600
+ name: "get_latest_digest",
601
+ description:
602
+ "Return the most recent DeepSQL daily digest for a connection (or the "
603
+ + "workspace if no connectionId). Digests are nightly summaries of slow "
604
+ + "queries + AI commentary written to Slack. Useful for context when the "
605
+ + "user asks 'what changed in the database recently?' without needing a "
606
+ + "fresh slow-query analysis.",
607
+ inputSchema: {
608
+ type: "object",
609
+ properties: {
610
+ connectionId: {
611
+ type: "string",
612
+ description: "Optional. Filter to one connection. Omit for workspace-wide.",
613
+ },
614
+ },
615
+ additionalProperties: false,
616
+ },
617
+ },
618
+ {
619
+ name: "list_digests",
620
+ description:
621
+ "Return the N most recent DeepSQL daily digests (compact metadata, not full "
622
+ + "body). Use to find the digest id for a date the user references "
623
+ + "('what was in yesterday's digest?'), then fetch the full content via "
624
+ + "`get_digest_by_id`.",
625
+ inputSchema: {
626
+ type: "object",
627
+ properties: {
628
+ connectionId: { type: "string", description: "Optional. Filter to one connection." },
629
+ count: { type: "integer", minimum: 1, maximum: 100, description: "Defaults to 10." },
630
+ },
631
+ additionalProperties: false,
632
+ },
633
+ },
634
+ {
635
+ name: "get_digest_by_id",
636
+ description:
637
+ "Return the full body of one DeepSQL daily digest by id, including the AI "
638
+ + "narrative and the slow-query list it was built from.",
639
+ inputSchema: {
640
+ type: "object",
641
+ properties: {
642
+ digestId: { type: "string", description: "Digest id (from list_digests)." },
643
+ connectionId: {
644
+ type: "string",
645
+ description: "Optional. Required only if multiple connections share digest ids.",
646
+ },
647
+ },
648
+ required: ["digestId"],
649
+ additionalProperties: false,
650
+ },
651
+ },
652
+ {
653
+ name: "get_missing_indexes",
654
+ description:
655
+ "Catalog probe: returns indexes the DeepSQL advisor thinks are MISSING "
656
+ + "based on live workload (joins to unindexed columns, sort/group on big "
657
+ + "tables, etc.). This is the schema-walk view — for the workload-weighted "
658
+ + "ROI-ranked recommendations (with HypoPG cost-delta), use "
659
+ + "`get_index_recommendations` instead.",
660
+ inputSchema: {
661
+ type: "object",
662
+ properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
663
+ required: ["connectionId"],
664
+ additionalProperties: false,
665
+ },
666
+ },
667
+ {
668
+ name: "get_index_health",
669
+ description:
670
+ "Comprehensive index health report for a connection: total indexes, "
671
+ + "bloated indexes, unused indexes, duplicate-prefix indexes, biggest "
672
+ + "indexes by size. Use as the first read when the user says 'audit my "
673
+ + "indexes' or 'are my indexes healthy?'.",
674
+ inputSchema: {
675
+ type: "object",
676
+ properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
677
+ required: ["connectionId"],
678
+ additionalProperties: false,
679
+ },
680
+ },
681
+ {
682
+ name: "get_unused_indexes",
683
+ description:
684
+ "Catalog probe: indexes the DB has reported zero (or near-zero) scans "
685
+ + "against since last reset. Each returned row has the table, index name, "
686
+ + "size, and scan count. Dropping these is a quick storage + write-cost "
687
+ + "win, but verify scan counters aren't recent before suggesting.",
688
+ inputSchema: {
689
+ type: "object",
690
+ properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
691
+ required: ["connectionId"],
692
+ additionalProperties: false,
693
+ },
694
+ },
695
+ {
696
+ name: "get_duplicate_indexes",
697
+ description:
698
+ "Catalog probe: indexes that are redundant prefixes of other indexes on "
699
+ + "the same table. Returns groups — each group is a set of indexes the "
700
+ + "optimizer would treat as interchangeable, with the recommendation to "
701
+ + "keep the longest/widest one.",
702
+ inputSchema: {
703
+ type: "object",
704
+ properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
705
+ required: ["connectionId"],
706
+ additionalProperties: false,
707
+ },
708
+ },
709
+ {
710
+ name: "get_table_index_usage",
711
+ description:
712
+ "Per-table index usage statistics: every index on the given table with its "
713
+ + "scan count, tuples read, and tuples fetched. Use to diagnose 'why isn't "
714
+ + "my index being used?' or to decide which of several composite indexes "
715
+ + "to drop.",
716
+ inputSchema: {
717
+ type: "object",
718
+ properties: {
719
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
720
+ tableName: { type: "string", description: "Table name (case-sensitive on Postgres)." },
721
+ },
722
+ required: ["connectionId", "tableName"],
723
+ additionalProperties: false,
724
+ },
725
+ },
726
+ {
727
+ name: "list_index_recommendations",
728
+ description:
729
+ "List ALL index recommendations for a connection, optionally filtered by "
730
+ + "status (PENDING / APPLIED / DISMISSED). For just the top-N pending ones "
731
+ + "with full evidence, use `get_index_recommendations` instead — that's the "
732
+ + "agent-facing entry point. This tool is for browsing the recommendation "
733
+ + "history (e.g. 'what did we apply last week?').",
734
+ inputSchema: {
735
+ type: "object",
736
+ properties: {
737
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
738
+ status: {
739
+ type: "string",
740
+ enum: ["PENDING", "APPLIED", "DISMISSED"],
741
+ description: "Optional. Defaults to all statuses.",
742
+ },
743
+ },
744
+ required: ["connectionId"],
745
+ additionalProperties: false,
746
+ },
747
+ },
748
+ {
749
+ name: "refresh_index_recommendations",
750
+ description:
751
+ "Force a fresh accumulation cycle: rescans the slow-query log for the "
752
+ + "lookback window and rebuilds the top-N index recommendations. Use when "
753
+ + "the user just deployed a new query pattern and wants to see if the "
754
+ + "advisor picks it up without waiting for the next 6-hour scheduler tick.",
755
+ inputSchema: {
756
+ type: "object",
757
+ properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
758
+ required: ["connectionId"],
759
+ additionalProperties: false,
760
+ },
761
+ },
762
+ {
763
+ name: "dismiss_index_recommendation",
764
+ description:
765
+ "Mark a recommendation as DISMISSED so it stops appearing in `top` / "
766
+ + "default `list` queries. Use when the user explicitly rejects a "
767
+ + "recommendation (not the same as APPLIED — dismissed means 'we decided "
768
+ + "not to'). Reversible by an admin editing the row directly.",
769
+ inputSchema: {
770
+ type: "object",
771
+ properties: {
772
+ recommendationId: { type: "string", description: "Recommendation row id." },
773
+ },
774
+ required: ["recommendationId"],
775
+ additionalProperties: false,
776
+ },
777
+ },
778
+ {
779
+ name: "get_latest_slow_query_analysis",
780
+ description:
781
+ "Return the most recently completed slow-query analysis run for a "
782
+ + "connection (the persisted result, no fresh collection). Faster than "
783
+ + "`analyze_slow_queries` because it doesn't trigger new work — use as "
784
+ + "the first read when investigating 'what's slow right now?', then "
785
+ + "fall back to `analyze_slow_queries` only if the latest is stale.",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: { connectionId: { type: "string", description: "DeepSQL connection ID." } },
789
+ required: ["connectionId"],
790
+ additionalProperties: false,
791
+ },
792
+ },
793
+ {
794
+ name: "list_slow_query_history",
795
+ description:
796
+ "List past slow-query analysis runs for a connection (compact metadata: "
797
+ + "id, timestamp, count, severity breakdown, AI-summary length). Use to "
798
+ + "find an older analysis to compare current state against, or to spot "
799
+ + "trends in how many slow queries are firing day-over-day.",
800
+ inputSchema: {
801
+ type: "object",
802
+ properties: {
803
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
804
+ limit: {
805
+ type: "integer",
806
+ minimum: 1,
807
+ maximum: 100,
808
+ description: "Number of past analyses to return. Defaults to 10.",
809
+ },
810
+ },
811
+ required: ["connectionId"],
812
+ additionalProperties: false,
813
+ },
814
+ },
815
+ {
816
+ name: "acknowledge_growth_anomaly",
817
+ description:
818
+ "Mark a detected growth anomaly as acknowledged so it stops appearing "
819
+ + "in `get_growth_anomalies(unacknowledgedOnly=true)`. Use after the user "
820
+ + "confirms the growth was expected (e.g., 'yes, we did a big backfill "
821
+ + "yesterday'). Does NOT delete the anomaly — it remains in history for "
822
+ + "audit/timeline purposes.",
823
+ inputSchema: {
824
+ type: "object",
825
+ properties: {
826
+ anomalyId: { type: "string", description: "Anomaly row id (from get_growth_anomalies)." },
827
+ },
828
+ required: ["anomalyId"],
829
+ additionalProperties: false,
830
+ },
831
+ },
832
+ {
833
+ name: "get_growth_config",
834
+ description:
835
+ "Return the alert thresholds and detection sensitivity currently "
836
+ + "configured for table-growth monitoring on a connection. Useful to "
837
+ + "explain to the user why a particular growth event did or didn't fire "
838
+ + "an anomaly.",
839
+ inputSchema: {
840
+ type: "object",
841
+ properties: {
842
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
843
+ },
844
+ required: ["connectionId"],
845
+ additionalProperties: false,
846
+ },
847
+ },
848
+ {
849
+ name: "set_growth_config",
850
+ description:
851
+ "Update the alert thresholds / sensitivity for growth monitoring on a "
852
+ + "connection. Admin-gated. The config body shape matches what "
853
+ + "`get_growth_config` returns — use that first to fetch current values, "
854
+ + "then submit a modified copy.",
855
+ inputSchema: {
856
+ type: "object",
857
+ properties: {
858
+ connectionId: { type: "string", description: "DeepSQL connection ID." },
859
+ config: {
860
+ type: "object",
861
+ description: "Full config object (see get_growth_config for shape).",
862
+ additionalProperties: true,
863
+ },
864
+ },
865
+ required: ["connectionId", "config"],
866
+ additionalProperties: false,
867
+ },
868
+ },
521
869
  ];
522
870
 
523
871
  class DeepSqlApiError extends Error {
@@ -1224,7 +1572,11 @@ function buildToolResult(name, payload, extra = {}) {
1224
1572
  text: summary,
1225
1573
  },
1226
1574
  ],
1227
- structuredContent: payload,
1575
+ // MCP spec requires `structuredContent` to be a JSON object. Several tools
1576
+ // (list_connections, get_relationships, list_business_rules, …) return a
1577
+ // top-level array from the backend; wrap those so spec-strict clients
1578
+ // (e.g. the `mcp` Python SDK used by Hermes) don't reject the result.
1579
+ structuredContent: Array.isArray(payload) ? { items: payload } : payload,
1228
1580
  };
1229
1581
  }
1230
1582
 
@@ -1557,6 +1909,211 @@ async function handleToolCall(config, name, args = {}) {
1557
1909
  return buildToolResult(name, payload);
1558
1910
  }
1559
1911
 
1912
+ // ─── Phase A symmetry — tools added to match the CLI surface ─────────
1913
+
1914
+ case "get_current_user": {
1915
+ const payload = await callDeepSqlApi(config, "/auth/me");
1916
+ return buildToolResult(name, payload);
1917
+ }
1918
+
1919
+ case "test_connection": {
1920
+ const connectionId = String(args.connectionId || "").trim();
1921
+ if (!connectionId) return buildToolError("connectionId is required.");
1922
+ // POST /connections/test with just { id } reuses the saved encrypted
1923
+ // creds server-side — no secrets cross the wire from the MCP client.
1924
+ const payload = await callDeepSqlApi(config, "/connections/test", {
1925
+ method: "POST",
1926
+ json: { id: connectionId },
1927
+ });
1928
+ return buildToolResult(name, payload);
1929
+ }
1930
+
1931
+ case "show_connection": {
1932
+ const connectionId = String(args.connectionId || "").trim();
1933
+ if (!connectionId) return buildToolError("connectionId is required.");
1934
+ // Backend has no GET /connections/{id}; list + filter mirrors CLI behavior.
1935
+ // Server returns secrets already masked.
1936
+ const all = await callDeepSqlApi(config, "/connections");
1937
+ const list = Array.isArray(all) ? all : [];
1938
+ const found = list.find((c) => (c.id || c.connectionId) === connectionId);
1939
+ if (!found) return buildToolError(`Connection ${connectionId} not found.`);
1940
+ return buildToolResult(name, found);
1941
+ }
1942
+
1943
+ case "reinit_connection_brain": {
1944
+ const connectionId = String(args.connectionId || "").trim();
1945
+ if (!connectionId) return buildToolError("connectionId is required.");
1946
+ const payload = await callDeepSqlApi(
1947
+ config,
1948
+ `/connections/${encodeURIComponent(connectionId)}/reinit`,
1949
+ { method: "POST", json: { force: args.force === true } },
1950
+ );
1951
+ return buildToolResult(name, payload);
1952
+ }
1953
+
1954
+ case "get_latest_digest": {
1955
+ const params = ["size=1"];
1956
+ if (args.connectionId) params.push(`connectionId=${encodeURIComponent(args.connectionId)}`);
1957
+ const payload = await callDeepSqlApi(config, `/admin/slack/digests?${params.join("&")}`);
1958
+ // Server returns a Page<>; surface the first element so consumers don't
1959
+ // have to know about Spring Data's content array.
1960
+ const first = Array.isArray(payload?.content) ? payload.content[0] : null;
1961
+ return buildToolResult(name, first || null);
1962
+ }
1963
+
1964
+ case "list_digests": {
1965
+ const count = clampInteger(args.count, 1, 100, 10);
1966
+ const params = [`size=${count}`];
1967
+ if (args.connectionId) params.push(`connectionId=${encodeURIComponent(args.connectionId)}`);
1968
+ const payload = await callDeepSqlApi(config, `/admin/slack/digests?${params.join("&")}`);
1969
+ return buildToolResult(name, Array.isArray(payload?.content) ? payload.content : []);
1970
+ }
1971
+
1972
+ case "get_digest_by_id": {
1973
+ const digestId = String(args.digestId || "").trim();
1974
+ if (!digestId) return buildToolError("digestId is required.");
1975
+ // No GET /admin/slack/digests/{id} — pull recent and filter.
1976
+ const params = ["size=100"];
1977
+ if (args.connectionId) params.push(`connectionId=${encodeURIComponent(args.connectionId)}`);
1978
+ const payload = await callDeepSqlApi(config, `/admin/slack/digests?${params.join("&")}`);
1979
+ const list = Array.isArray(payload?.content) ? payload.content : [];
1980
+ const found = list.find((d) => String(d.id) === digestId);
1981
+ if (!found) return buildToolError(`Digest ${digestId} not in the recent 100 digests.`);
1982
+ return buildToolResult(name, found);
1983
+ }
1984
+
1985
+ case "get_missing_indexes": {
1986
+ const connectionId = String(args.connectionId || "").trim();
1987
+ if (!connectionId) return buildToolError("connectionId is required.");
1988
+ const payload = await callDeepSqlApi(
1989
+ config, `/advisor/indexes/${encodeURIComponent(connectionId)}`);
1990
+ return buildToolResult(name, payload);
1991
+ }
1992
+
1993
+ case "get_index_health": {
1994
+ const connectionId = String(args.connectionId || "").trim();
1995
+ if (!connectionId) return buildToolError("connectionId is required.");
1996
+ const payload = await callDeepSqlApi(
1997
+ config, `/index-advisor/${encodeURIComponent(connectionId)}/health-report`);
1998
+ return buildToolResult(name, payload);
1999
+ }
2000
+
2001
+ case "get_unused_indexes": {
2002
+ const connectionId = String(args.connectionId || "").trim();
2003
+ if (!connectionId) return buildToolError("connectionId is required.");
2004
+ const payload = await callDeepSqlApi(
2005
+ config, `/index-advisor/${encodeURIComponent(connectionId)}/unused`);
2006
+ return buildToolResult(name, payload);
2007
+ }
2008
+
2009
+ case "get_duplicate_indexes": {
2010
+ const connectionId = String(args.connectionId || "").trim();
2011
+ if (!connectionId) return buildToolError("connectionId is required.");
2012
+ const payload = await callDeepSqlApi(
2013
+ config, `/index-advisor/${encodeURIComponent(connectionId)}/duplicates`);
2014
+ return buildToolResult(name, payload);
2015
+ }
2016
+
2017
+ case "get_table_index_usage": {
2018
+ const connectionId = String(args.connectionId || "").trim();
2019
+ const tableName = String(args.tableName || "").trim();
2020
+ if (!connectionId) return buildToolError("connectionId is required.");
2021
+ if (!tableName) return buildToolError("tableName is required.");
2022
+ const payload = await callDeepSqlApi(
2023
+ config,
2024
+ `/index-advisor/${encodeURIComponent(connectionId)}/usage/${encodeURIComponent(tableName)}`,
2025
+ );
2026
+ return buildToolResult(name, payload);
2027
+ }
2028
+
2029
+ case "list_index_recommendations": {
2030
+ const connectionId = String(args.connectionId || "").trim();
2031
+ if (!connectionId) return buildToolError("connectionId is required.");
2032
+ // The "list ALL" path returns every status; we filter client-side
2033
+ // for PENDING/APPLIED/DISMISSED to match the CLI semantics.
2034
+ const all = await callDeepSqlApi(
2035
+ config, `/index-recommendations/${encodeURIComponent(connectionId)}`);
2036
+ const list = Array.isArray(all) ? all : [];
2037
+ const filtered = args.status
2038
+ ? list.filter((r) => String(r.status || "").toUpperCase() === String(args.status).toUpperCase())
2039
+ : list;
2040
+ return buildToolResult(name, filtered);
2041
+ }
2042
+
2043
+ case "refresh_index_recommendations": {
2044
+ const connectionId = String(args.connectionId || "").trim();
2045
+ if (!connectionId) return buildToolError("connectionId is required.");
2046
+ const payload = await callDeepSqlApi(
2047
+ config,
2048
+ `/index-recommendations/generate/${encodeURIComponent(connectionId)}`,
2049
+ { method: "POST" },
2050
+ );
2051
+ return buildToolResult(name, payload);
2052
+ }
2053
+
2054
+ case "dismiss_index_recommendation": {
2055
+ const recommendationId = String(args.recommendationId || "").trim();
2056
+ if (!recommendationId) return buildToolError("recommendationId is required.");
2057
+ const payload = await callDeepSqlApi(
2058
+ config,
2059
+ `/index-recommendations/${encodeURIComponent(recommendationId)}/dismiss`,
2060
+ { method: "PUT" },
2061
+ );
2062
+ return buildToolResult(name, payload);
2063
+ }
2064
+
2065
+ case "get_latest_slow_query_analysis": {
2066
+ const connectionId = String(args.connectionId || "").trim();
2067
+ if (!connectionId) return buildToolError("connectionId is required.");
2068
+ const payload = await callDeepSqlApi(
2069
+ config, `/slow-queries/latest/${encodeURIComponent(connectionId)}`);
2070
+ return buildToolResult(name, payload);
2071
+ }
2072
+
2073
+ case "list_slow_query_history": {
2074
+ const connectionId = String(args.connectionId || "").trim();
2075
+ if (!connectionId) return buildToolError("connectionId is required.");
2076
+ const limit = clampInteger(args.limit, 1, 100, 10);
2077
+ const payload = await callDeepSqlApi(
2078
+ config,
2079
+ `/slow-queries/history/${encodeURIComponent(connectionId)}?limit=${limit}`,
2080
+ );
2081
+ return buildToolResult(name, payload);
2082
+ }
2083
+
2084
+ case "acknowledge_growth_anomaly": {
2085
+ const anomalyId = String(args.anomalyId || "").trim();
2086
+ if (!anomalyId) return buildToolError("anomalyId is required.");
2087
+ const payload = await callDeepSqlApi(
2088
+ config,
2089
+ `/growth-monitoring/anomalies/${encodeURIComponent(anomalyId)}/acknowledge`,
2090
+ { method: "POST" },
2091
+ );
2092
+ return buildToolResult(name, payload);
2093
+ }
2094
+
2095
+ case "get_growth_config": {
2096
+ const connectionId = String(args.connectionId || "").trim();
2097
+ if (!connectionId) return buildToolError("connectionId is required.");
2098
+ const payload = await callDeepSqlApi(
2099
+ config, `/growth-monitoring/config/${encodeURIComponent(connectionId)}`);
2100
+ return buildToolResult(name, payload);
2101
+ }
2102
+
2103
+ case "set_growth_config": {
2104
+ const connectionId = String(args.connectionId || "").trim();
2105
+ if (!connectionId) return buildToolError("connectionId is required.");
2106
+ if (!args.config || typeof args.config !== "object") {
2107
+ return buildToolError("config object is required (see get_growth_config for shape).");
2108
+ }
2109
+ const payload = await callDeepSqlApi(
2110
+ config,
2111
+ "/growth-monitoring/config",
2112
+ { method: "POST", json: { ...args.config, connectionId } },
2113
+ );
2114
+ return buildToolResult(name, payload);
2115
+ }
2116
+
1560
2117
  default:
1561
2118
  return buildToolError(`Unknown tool: ${name}`);
1562
2119
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepsql/mcp",
3
- "version": "0.18.2",
4
- "description": "DeepSQL CLI and stdio MCP server for self-hosted deployments",
3
+ "version": "0.20.0",
4
+ "description": "DeepSQL CLI, DBA Agent TUI, and stdio MCP server for self-hosted deployments",
5
5
  "bin": {
6
6
  "deepsql": "bin/deepsql.js",
7
7
  "deepsql-mcp": "deepsql-phase1-server.js"
@@ -14,6 +14,7 @@
14
14
  "bin",
15
15
  "skills",
16
16
  "src",
17
+ "agent-profile",
17
18
  "deepsql-phase1-server.js",
18
19
  "deepsql-phase1-lib.js",
19
20
  "claude_desktop_config.customer.example.json",
@@ -2,13 +2,18 @@
2
2
 
3
3
  You have two DeepSQL surfaces available:
4
4
 
5
- 1. **MCP tools** — JSON-RPC tools loaded into your session (21 of them).
6
- Use these for in-session retrieval and SQL execution.
5
+ 1. **MCP tools** — JSON-RPC tools loaded into your session (41 of them
6
+ as of 0.19.0). The MCP surface mirrors the CLI for almost every
7
+ read/diagnostic operation, plus the two execute tools and the index-
8
+ apply tool.
7
9
 
8
10
  2. **`deepsql` CLI** — a shell binary on the user's PATH (~19 commands).
9
- Use this for things the MCP doesn't expose (index health, daily
10
- digest, slow-query streaming, connection management, admin ops, the
11
- plan-analysis `analyze` command, etc.). You can shell out to the CLI
11
+ Use this for the few things still CLI-only: auth (`login`/`logout`),
12
+ the MCP installer (`deepsql mcp config --install`), connection
13
+ CRUD (anything that takes plaintext DB credentials those must
14
+ stay out of your conversation history), the `setup` wizard, the
15
+ streaming `slow-queries optimize` SSE flow, and per-user admin ops
16
+ (`users`/`access`/`permissions`). You can shell out to the CLI
12
17
  yourself when appropriate; you can also point the user at the
13
18
  command if it's interactive.
14
19
 
@@ -93,11 +98,26 @@ usually doesn't know about either; that's exactly why DeepSQL exists.
93
98
  | `get_slow_query_customers(connectionId)` | Tenants ranked by total slow-query time — answers "which customer is driving the load?" |
94
99
  | `get_query_samples(connectionId, fingerprint)` | Literal SQL samples with bind values for a fingerprint, slowest-first. Use to reproduce an execution or run a real EXPLAIN. |
95
100
  | `get_slow_query_insights(connectionId, kind?, window?, limit?)` | Pre-computed AI insights — `hotspots`, `remediation`, `tail-risk`, `plan-drift`, `skew`, or `all` (default). |
96
- | `optimize_slow_query(connectionId, queryText, avgExecutionTimeMs?)` | AI optimization recommendations for a specific SQL — index suggestions, query rewrites, estimated impact. |
101
+ | `optimize_slow_query(connectionId, queryText, avgExecutionTimeMs?)` | AI query REWRITE + plan diagnosis for one SQL (single-query scoped). NOT indexes those need the whole workload; use `get_index_recommendations` or Workload Analysis. |
97
102
  | `get_table_growth(connectionId, tableName?, days?)` | Persistent stats history: per-table size/row time series + headline rollups. Use to answer "which tables are growing fastest?" or "how much has X grown in the last month?" without scanning the live DB. |
98
103
  | `get_growth_anomalies(connectionId, tableName?, unacknowledgedOnly?, days?)` | DeepSQL-flagged sudden growth spikes with severity (CRITICAL/WARNING/INFO), anomaly type, before/after sizes, confidence score. Check this BEFORE walking the user through a slow-query plan — a recent growth anomaly is often the real root cause. |
99
104
  | `execute_sql(connectionId, query, ...)` | Run any SQL — SELECT for everyone, DML/DDL for admins (two-step confirm). |
100
105
  | `analyze_query_plan(connectionId, query, useAnalyze=false)` | AI-enriched plan analysis (issues + index recs + summary). |
106
+ | `get_current_user()` | Authenticated user + role. Use to know whether the caller is admin-capable before suggesting DDL. |
107
+ | `test_connection(connectionId)` | Validates a saved connection (privilege report + SSH tunnel check). Read-only on the customer's DB. |
108
+ | `show_connection(connectionId)` | Full saved config with secrets masked. Diagnose host/port/SSL/SSH issues. |
109
+ | `reinit_connection_brain(connectionId, force?)` | Trigger a fresh schema scan + brain re-embedding. Use after the user reports stale schema knowledge. |
110
+ | `get_latest_digest(connectionId?)` / `list_digests(...)` / `get_digest_by_id(digestId, ...)` | Daily DeepSQL digests (slow queries + AI commentary written nightly to Slack). |
111
+ | `get_missing_indexes(connectionId)` | Schema-walk view of suspected missing indexes (complements the workload-weighted `get_index_recommendations`). |
112
+ | `get_index_health(connectionId)` | Total/bloated/unused/duplicate/biggest indexes. Use as first read on "audit my indexes". |
113
+ | `get_unused_indexes(connectionId)` / `get_duplicate_indexes(connectionId)` | Catalog probes for storage + write-cost wins. |
114
+ | `get_table_index_usage(connectionId, tableName)` | Per-table index scan/read/fetch counts. Diagnose "why isn't my index being used?". |
115
+ | `list_index_recommendations(connectionId, status?)` | Browse the full recommendation history (PENDING/APPLIED/DISMISSED). |
116
+ | `refresh_index_recommendations(connectionId)` | Force a fresh accumulation cycle (skips the 6-hour scheduler wait). |
117
+ | `dismiss_index_recommendation(recommendationId)` | Reject a recommendation. Use only after the user explicitly says no. |
118
+ | `get_latest_slow_query_analysis(connectionId)` / `list_slow_query_history(...)` | Read persisted slow-query analysis runs (faster than `analyze_slow_queries` which triggers new work). |
119
+ | `acknowledge_growth_anomaly(anomalyId)` | Mark a growth alert as expected. Use after the user confirms the growth was intentional. |
120
+ | `get_growth_config(connectionId)` / `set_growth_config(connectionId, config)` | View/edit growth-monitoring alert thresholds. |
101
121
 
102
122
  `EXPLAIN` and `EXPLAIN ANALYZE` are just SQL — type them as the query if
103
123
  you want raw plan output. Use `analyze_query_plan` when you want the
@@ -118,10 +138,11 @@ called by claude-code via deepsql CLI").
118
138
  deepsql query "SELECT 1" --connection prod-pg --caller-agent claude-code --json
119
139
  ```
120
140
 
121
- ### Command catalog (19 top-level commands)
141
+ ### Command catalog (20 top-level commands)
122
142
 
123
143
  | Command | What it does | MCP equivalent? |
124
144
  |---|---|---|
145
+ | `deepsql agent` (or bare `deepsql` in a terminal) | Launch the **DeepSQL Agent** — an interactive DBA/BI chat TUI. Uses your saved `deepsql login`; the model is proxied by the DeepSQL backend, so no LLM key is needed. First run installs the agent runtime. | none — interactive TUI |
125
146
  | `deepsql login` | Authorize CLI against a DeepSQL host (browser PKCE / device code / password) | none — interactive only |
126
147
  | `deepsql logout` | Revoke the saved token | none |
127
148
  | `deepsql whoami` | Show the logged-in user, role, URL, pinned connection | none |