dbmcp_postgres/tools/
list_procedures.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::{ListProceduresResponse, PinnedListProceduresRequest, UnpinnedListProceduresRequest};
12
13const NAME: &str = "listProcedures";
14const TITLE: &str = "List Procedures";
15const DESCRIPTION_PINNED: &str = include_str!("../../assets/tools/list_procedures/pinned.md");
16const DESCRIPTION_UNPINNED: &str = include_str!("../../assets/tools/list_procedures/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 PinnedListProceduresTool;
28
29impl ToolBase for PinnedListProceduresTool {
30 type Parameter = PinnedListProceduresRequest;
31 type Output = ListProceduresResponse;
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 PinnedListProceduresTool {
52 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
53 handler
54 .list_procedures(None, params.cursor, params.search, params.detailed)
55 .await
56 }
57}
58
59pub(crate) struct UnpinnedListProceduresTool;
61
62impl ToolBase for UnpinnedListProceduresTool {
63 type Parameter = UnpinnedListProceduresRequest;
64 type Output = ListProceduresResponse;
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 UnpinnedListProceduresTool {
85 async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
86 handler
87 .list_procedures(
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 = 'p'
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"
130 SELECT
131 p.proname || '(' || args.text || ')' AS name,
132 json_build_object(
133 'schema', 'public',
134 'name', p.proname,
135 'language', l.lanname,
136 'arguments', args.text,
137 'security', CASE WHEN p.prosecdef THEN 'DEFINER' ELSE 'INVOKER' END,
138 'owner', r.rolname,
139 'description', pg_catalog.obj_description(p.oid, 'pg_proc'),
140 'definition', pg_get_functiondef(p.oid)
141 ) AS entry
142 FROM pg_proc p
143 JOIN pg_namespace n ON n.oid = p.pronamespace
144 JOIN pg_language l ON l.oid = p.prolang
145 JOIN pg_roles r ON r.oid = p.proowner
146 CROSS JOIN LATERAL (SELECT pg_get_function_arguments(p.oid) AS text) args
147 WHERE n.nspname = 'public'
148 AND p.prokind = 'p'
149 AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
150 ORDER BY p.proname, p.oid
151 LIMIT $2 OFFSET $3";
152
153impl PostgresHandler {
154 pub async fn list_procedures(
162 &self,
163 database: Option<String>,
164 cursor: Option<Cursor>,
165 search: Option<String>,
166 detailed: bool,
167 ) -> Result<ListProceduresResponse, ErrorData> {
168 let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
169 let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
170 let pager = Pager::new(cursor, self.config.page_size);
171
172 if detailed {
173 let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
174 .connection
175 .fetch(
176 sqlx::query(DETAILED_SQL)
177 .bind(pattern)
178 .bind(pager.limit())
179 .bind(pager.offset()),
180 database,
181 )
182 .await?;
183 let (rows, next_cursor) = pager.paginate(rows);
184 return Ok(ListProceduresResponse::detailed(
185 rows.into_iter().map(|(key, json)| (key, json.0)).collect(),
186 next_cursor,
187 ));
188 }
189
190 let rows: Vec<String> = self
191 .connection
192 .fetch_scalar(
193 sqlx::query(BRIEF_SQL)
194 .bind(pattern)
195 .bind(pager.limit())
196 .bind(pager.offset()),
197 database,
198 )
199 .await?;
200 let (procedures, next_cursor) = pager.paginate(rows);
201 Ok(ListProceduresResponse::brief(procedures, next_cursor))
202 }
203}