1use std::borrow::Cow;
4
5use dbmcp_server::pagination::Pager;
6use dbmcp_sql::Connection as _;
7use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
8use rmcp::model::{ErrorData, ToolAnnotations};
9
10use crate::SqliteHandler;
11use crate::types::{ListTablesRequest, ListTablesResponse};
12
13pub(crate) struct ListTablesTool;
15
16impl ListTablesTool {
17 const NAME: &'static str = "listTables";
18 const TITLE: &'static str = "List Tables";
19 const DESCRIPTION: &'static str = r#"List tables in the connected database, optionally filtered and/or with full metadata.
20
21<usecase>
22Use when:
23- Exploring a database to find relevant tables (brief mode, default).
24- Searching for a table by partial name (pass `search`).
25- Inspecting a table's columns, constraints, indexes, and triggers before writing a query (pass `detailed: true`). This supersedes the legacy `getTableSchema` workflow.
26</usecase>
27
28<parameters>
29- `cursor` — Opaque pagination cursor; echo the prior response's `nextCursor`.
30- `search` — Case-insensitive filter on table names via `LIKE`. `%` matches any sequence; `_` matches a single character.
31- `detailed` — When `true`, returns full metadata objects keyed by table name instead of bare name strings. Default `false`.
32</parameters>
33
34<examples>
35✓ "What tables are in this database?" → listTables()
36✓ "Find the orders table" → listTables(search="order")
37✓ "What columns does orders have?" → listTables(search="orders", detailed=true)
38</examples>
39
40<what_it_returns>
41Brief mode (default): a sorted JSON array of table-name strings, e.g. `["customers", "orders"]`.
42Detailed mode: a JSON object keyed by table name; each value carries `schema`, `kind`, `owner`, `comment`, `columns`, `constraints`, `indexes`, and `triggers` (the name is the key and is not repeated inside the value). Keys iterate in the same alphabetical order brief mode uses. Example: `{"orders": {"schema": "main", "kind": "TABLE", ...}}`.
43</what_it_returns>
44
45<pagination>
46Paginated. Pass the prior response's `nextCursor` as `cursor` to fetch the next page. The `search` filter must stay the same across pages for cursor continuity.
47</pagination>"#;
48}
49
50impl ToolBase for ListTablesTool {
51 type Parameter = ListTablesRequest;
52 type Output = ListTablesResponse;
53 type Error = ErrorData;
54
55 fn name() -> Cow<'static, str> {
56 Self::NAME.into()
57 }
58
59 fn title() -> Option<String> {
60 Some(Self::TITLE.into())
61 }
62
63 fn description() -> Option<Cow<'static, str>> {
64 Some(Self::DESCRIPTION.into())
65 }
66
67 fn annotations() -> Option<ToolAnnotations> {
68 Some(
69 ToolAnnotations::new()
70 .read_only(true)
71 .destructive(false)
72 .idempotent(true)
73 .open_world(false),
74 )
75 }
76}
77
78impl AsyncTool<SqliteHandler> for ListTablesTool {
79 async fn invoke(handler: &SqliteHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
80 handler.list_tables(params).await
81 }
82}
83
84const BRIEF_SQL: &str = r"
100 SELECT tl.name
101 FROM pragma_table_list tl
102 WHERE tl.schema = 'main'
103 AND tl.type IN ('table', 'virtual')
104 AND tl.name NOT LIKE 'sqlite_%'
105 AND (?1 IS NULL OR tl.name LIKE '%' || ?1 || '%')
106 ORDER BY tl.name
107 LIMIT ?2 OFFSET ?3";
108
109const DETAILED_SQL: &str = r#"
126 WITH table_info AS (
127 SELECT
128 tl.name AS table_name,
129 CASE tl.type
130 WHEN 'virtual' THEN 'VIRTUAL_TABLE'
131 ELSE 'TABLE'
132 END AS kind
133 FROM pragma_table_list tl
134 WHERE tl.schema = 'main'
135 AND tl.type IN ('table', 'virtual')
136 AND tl.name NOT LIKE 'sqlite_%'
137 AND (?1 IS NULL OR tl.name LIKE '%' || ?1 || '%')
138 ORDER BY tl.name
139 LIMIT ?2 OFFSET ?3
140 ),
141 columns_info AS (
142 SELECT
143 ti.table_name,
144 c.cid AS cid,
145 json_object(
146 'name', c.name,
147 'dataType', c.type,
148 'ordinalPosition', c.cid + 1,
149 'nullable', json(CASE WHEN c."notnull" = 0 THEN 'true' ELSE 'false' END),
150 'default', c.dflt_value,
151 'comment', NULL
152 ) AS column_json
153 FROM table_info ti, pragma_table_info(ti.table_name) c
154 ),
155 pk_constraints AS (
156 SELECT
157 ti.table_name,
158 json_object(
159 'name', 'PRIMARY',
160 'type', 'PRIMARY KEY',
161 'columns', json_group_array(c.name),
162 'definition', 'PRIMARY KEY (' || group_concat('"' || c.name || '"', ', ') || ')'
163 ) AS constraint_json
164 FROM table_info ti, pragma_table_info(ti.table_name) c
165 WHERE c.pk > 0
166 GROUP BY ti.table_name
167 HAVING COUNT(c.name) > 0
168 ),
169 fk_constraints AS (
170 SELECT
171 ti.table_name,
172 json_object(
173 'name', 'fk_' || ti.table_name || '_' || f.id,
174 'type', 'FOREIGN KEY',
175 'columns', json_group_array(f."from"),
176 'definition', 'FOREIGN KEY (' || group_concat('"' || f."from" || '"', ', ')
177 || ') REFERENCES "' || f."table" || '"('
178 || group_concat('"' || f."to" || '"', ', ') || ')',
179 'referencedTable', f."table",
180 'referencedColumns', json_group_array(f."to")
181 ) AS constraint_json
182 FROM table_info ti, pragma_foreign_key_list(ti.table_name) f
183 GROUP BY ti.table_name, f.id
184 ),
185 unique_constraints AS (
186 SELECT
187 ti.table_name,
188 json_object(
189 'name', i.name,
190 'type', 'UNIQUE',
191 'columns', (SELECT json_group_array(ii.name)
192 FROM pragma_index_info(i.name) ii),
193 'definition', 'UNIQUE ('
194 || (SELECT group_concat('"' || ii.name || '"', ', ')
195 FROM pragma_index_info(i.name) ii)
196 || ')'
197 ) AS constraint_json
198 FROM table_info ti, pragma_index_list(ti.table_name) i
199 WHERE i."unique" = 1 AND i.origin <> 'pk'
200 ),
201 all_constraints AS (
202 SELECT table_name, constraint_json FROM pk_constraints
203 UNION ALL
204 SELECT table_name, constraint_json FROM fk_constraints
205 UNION ALL
206 SELECT table_name, constraint_json FROM unique_constraints
207 ),
208 indexes_info AS (
209 SELECT
210 ti.table_name,
211 json_object(
212 'name', il.name,
213 'columns', (SELECT json_group_array(ii.name) FROM pragma_index_info(il.name) ii),
214 'unique', json(CASE WHEN il."unique" = 1 THEN 'true' ELSE 'false' END),
215 'primary', json(CASE WHEN il.origin = 'pk' THEN 'true' ELSE 'false' END),
216 'method', 'btree',
217 'definition', COALESCE(
218 (SELECT m2.sql FROM sqlite_master m2 WHERE m2.type = 'index' AND m2.name = il.name),
219 'CREATE ' || CASE il."unique" WHEN 1 THEN 'UNIQUE INDEX ' ELSE 'INDEX ' END
220 || '"' || il.name || '" ON "' || ti.table_name || '"('
221 || (SELECT group_concat('"' || ii.name || '"', ', ') FROM pragma_index_info(il.name) ii)
222 || ')'
223 )
224 ) AS index_json
225 FROM table_info ti, pragma_index_list(ti.table_name) il
226 ),
227 triggers_info AS (
228 SELECT
229 m.tbl_name AS table_name,
230 json_object(
231 'name', m.name,
232 'definition', m.sql,
233 'enabled', json('true')
234 ) AS trigger_json
235 FROM sqlite_master m
236 JOIN table_info ti ON ti.table_name = m.tbl_name
237 WHERE m.type = 'trigger'
238 )
239 SELECT
240 ti.table_name AS name,
241 json_object(
242 'schema', 'main',
243 'kind', ti.kind,
244 'owner', NULL,
245 'comment', NULL,
246 'columns', COALESCE(
247 (SELECT json_group_array(json(ci.column_json))
248 FROM (SELECT column_json
249 FROM columns_info
250 WHERE table_name = ti.table_name
251 ORDER BY cid) ci),
252 json('[]')),
253 'constraints', COALESCE(
254 (SELECT json_group_array(json(ac.constraint_json))
255 FROM all_constraints ac
256 WHERE ac.table_name = ti.table_name),
257 json('[]')),
258 'indexes', COALESCE(
259 (SELECT json_group_array(json(ii.index_json))
260 FROM indexes_info ii
261 WHERE ii.table_name = ti.table_name),
262 json('[]')),
263 'triggers', COALESCE(
264 (SELECT json_group_array(json(tg.trigger_json))
265 FROM triggers_info tg
266 WHERE tg.table_name = ti.table_name),
267 json('[]'))
268 ) AS entry
269 FROM table_info ti
270 ORDER BY ti.table_name"#;
271
272impl SqliteHandler {
273 pub async fn list_tables(
280 &self,
281 ListTablesRequest {
282 cursor,
283 search,
284 detailed,
285 }: ListTablesRequest,
286 ) -> Result<ListTablesResponse, ErrorData> {
287 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
288 let pager = Pager::new(cursor, self.config.page_size);
289
290 if detailed {
291 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
292 .connection
293 .fetch(
294 sqlx::query(DETAILED_SQL)
295 .bind(pattern)
296 .bind(pager.limit())
297 .bind(pager.offset()),
298 None,
299 )
300 .await?;
301 let (rows, next_cursor) = pager.paginate(rows);
302 return Ok(ListTablesResponse::detailed(
303 rows.into_iter().map(|(name, json)| (name, json.0)).collect(),
304 next_cursor,
305 ));
306 }
307
308 let rows: Vec<String> = self
309 .connection
310 .fetch_scalar(
311 sqlx::query(BRIEF_SQL)
312 .bind(pattern)
313 .bind(pager.limit())
314 .bind(pager.offset()),
315 None,
316 )
317 .await?;
318 let (tables, next_cursor) = pager.paginate(rows);
319 Ok(ListTablesResponse::brief(tables, next_cursor))
320 }
321}