dbmcp_postgres/tools/
list_functions.rs1use std::borrow::Cow;
4
5use dbmcp_server::pagination::{Cursor, Pager};
6use dbmcp_sql::Connection as _;
7use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
8use rmcp::model::{ErrorData, ToolAnnotations};
9
10use crate::PostgresHandler;
11use crate::types::{ListFunctionsResponse, PinnedListFunctionsRequest, UnpinnedListFunctionsRequest};
12
13const NAME: &str = "listFunctions";
14const TITLE: &str = "List Functions";
15const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/list_functions/pinned.md");
16const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/list_functions/unpinned.md");
17
18fn annotations() -> ToolAnnotations {
19 ToolAnnotations::new()
20 .read_only(true)
21 .destructive(false)
22 .idempotent(true)
23 .open_world(false)
24}
25
26pub(crate) struct PinnedListFunctionsTool;
28
29impl ToolBase for PinnedListFunctionsTool {
30 type Parameter = PinnedListFunctionsRequest;
31 type Output = ListFunctionsResponse;
32 type Error = ErrorData;
33
34 fn name() -> Cow<'static, str> {
35 NAME.into()
36 }
37
38 fn title() -> Option<String> {
39 Some(TITLE.into())
40 }
41
42 fn description() -> Option<Cow<'static, str>> {
43 Some(DESCRIPTION_PINNED.into())
44 }
45
46 fn annotations() -> Option<ToolAnnotations> {
47 Some(annotations())
48 }
49}
50
51impl AsyncTool<PostgresHandler> for PinnedListFunctionsTool {
52 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
53 handler
54 .list_functions(None, params.cursor, params.search, params.detailed)
55 .await
56 }
57}
58
59pub(crate) struct UnpinnedListFunctionsTool;
61
62impl ToolBase for UnpinnedListFunctionsTool {
63 type Parameter = UnpinnedListFunctionsRequest;
64 type Output = ListFunctionsResponse;
65 type Error = ErrorData;
66
67 fn name() -> Cow<'static, str> {
68 NAME.into()
69 }
70
71 fn title() -> Option<String> {
72 Some(TITLE.into())
73 }
74
75 fn description() -> Option<Cow<'static, str>> {
76 Some(DESCRIPTION_UNPINNED.into())
77 }
78
79 fn annotations() -> Option<ToolAnnotations> {
80 Some(annotations())
81 }
82}
83
84impl AsyncTool<PostgresHandler> for UnpinnedListFunctionsTool {
85 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
86 handler
87 .list_functions(
88 params.database,
89 params.inner.cursor,
90 params.inner.search,
91 params.inner.detailed,
92 )
93 .await
94 }
95}
96
97const BRIEF_SQL: &str = r"
105 SELECT p.proname
106 FROM pg_proc p
107 JOIN pg_namespace n ON n.oid = p.pronamespace
108 WHERE n.nspname = 'public'
109 AND p.prokind = 'f'
110 AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
111 ORDER BY p.proname, p.oid
112 LIMIT $2 OFFSET $3";
113
114const DETAILED_SQL: &str = r"
133 SELECT
134 p.proname || '(' || args.text || ')' AS name,
135 json_build_object(
136 'schema', 'public',
137 'name', p.proname,
138 'language', l.lanname,
139 'arguments', args.text,
140 'returnType', pg_get_function_result(p.oid),
141 'volatility', CASE p.provolatile
142 WHEN 'i' THEN 'IMMUTABLE'
143 WHEN 's' THEN 'STABLE'
144 WHEN 'v' THEN 'VOLATILE'
145 END,
146 'strict', p.proisstrict,
147 'security', CASE WHEN p.prosecdef THEN 'DEFINER' ELSE 'INVOKER' END,
148 'parallelSafety', CASE p.proparallel
149 WHEN 's' THEN 'SAFE'
150 WHEN 'r' THEN 'RESTRICTED'
151 WHEN 'u' THEN 'UNSAFE'
152 END,
153 'owner', r.rolname,
154 'description', pg_catalog.obj_description(p.oid, 'pg_proc'),
155 'definition', pg_get_functiondef(p.oid)
156 ) AS entry
157 FROM pg_proc p
158 JOIN pg_namespace n ON n.oid = p.pronamespace
159 JOIN pg_language l ON l.oid = p.prolang
160 JOIN pg_roles r ON r.oid = p.proowner
161 CROSS JOIN LATERAL (SELECT pg_get_function_arguments(p.oid) AS text) args
162 WHERE n.nspname = 'public'
163 AND p.prokind = 'f'
164 AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
165 ORDER BY p.proname, p.oid
166 LIMIT $2 OFFSET $3";
167
168impl PostgresHandler {
169 pub async fn list_functions(
177 &self,
178 database: Option<String>,
179 cursor: Option<Cursor>,
180 search: Option<String>,
181 detailed: bool,
182 ) -> Result<ListFunctionsResponse, ErrorData> {
183 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
184 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
185 let pager = Pager::new(cursor, self.config.page_size);
186
187 if detailed {
188 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
189 .connection
190 .fetch(
191 sqlx::query(DETAILED_SQL)
192 .bind(pattern)
193 .bind(pager.limit())
194 .bind(pager.offset()),
195 database,
196 )
197 .await?;
198 let (rows, next_cursor) = pager.paginate(rows);
199 return Ok(ListFunctionsResponse::detailed(
200 rows.into_iter().map(|(key, json)| (key, json.0)).collect(),
201 next_cursor,
202 ));
203 }
204
205 let rows: Vec<String> = self
206 .connection
207 .fetch_scalar(
208 sqlx::query(BRIEF_SQL)
209 .bind(pattern)
210 .bind(pager.limit())
211 .bind(pager.offset()),
212 database,
213 )
214 .await?;
215 let (functions, next_cursor) = pager.paginate(rows);
216 Ok(ListFunctionsResponse::brief(functions, next_cursor))
217 }
218}