Skip to main content

dbmcp_postgres/tools/
list_functions.rs

1//! MCP tool: `listFunctions`.
2
3use 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
26/// Marker type for the `listFunctions` MCP tool (pinned variant — no `database` field).
27pub(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
59/// Marker type for the `listFunctions` MCP tool (unpinned variant — carries `database`).
60pub(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
97/// Brief-mode SQL: `pg_proc` scan with `ILIKE` filter on function name.
98///
99/// `n.nspname = 'public'` plus `p.prokind = 'f'` keep aggregates / window
100/// functions / procedures out of the result. The `($1::text IS NULL OR ...)`
101/// trinary lets one statement cover both filtered and unfiltered cases.
102/// `(p.proname, p.oid)` is the sort key — `oid` is the unique tiebreaker
103/// across overloaded names so `OFFSET` pagination is deterministic.
104const 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
114/// Detailed-mode SQL: per-function `json_build_object` projection.
115///
116/// `n.nspname = 'public'` and `p.prokind = 'f'` filter to user-defined
117/// functions in the `public` schema. Three small lookup joins (`pg_namespace`,
118/// `pg_language`, `pg_roles`) supply the language and owner names. Postgres
119/// defers SELECT-list evaluation past `LIMIT`, so the expensive `pg_get_*`
120/// projections (`pg_get_functiondef`, `pg_get_function_arguments`,
121/// `pg_get_function_result`) and `obj_description` only run for the page's
122/// rows — never the full schema.
123///
124/// A `CROSS JOIN LATERAL` materialises `pg_get_function_arguments(p.oid)`
125/// into `args.text` so it is computed once per row and reused both in the
126/// keyed signature `name(args)` and in the JSON `arguments` field — no
127/// double-call, no Rust-side json lookup.
128///
129/// `provolatile`, `proisstrict`, `prosecdef`, `proparallel` are stable
130/// `pg_proc` columns. `provolatile` is `'i'`/`'s'`/`'v'`; `proparallel` is
131/// `'s'`/`'r'`/`'u'`; `prosecdef` is a boolean.
132const 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    /// Lists one page of user-defined functions, optionally filtered and/or detailed.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`ErrorData`] with code `-32602` if `cursor` is malformed,
174    /// or an internal-error [`ErrorData`] if `database` is invalid
175    /// or the underlying query fails.
176    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}