Skip to main content

dbmcp_postgres/tools/
list_functions.rs

1//! MCP tool: `listFunctions`.
2
3use 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::PostgresHandler;
11use crate::types::{ListFunctionsRequest, ListFunctionsResponse};
12
13/// Marker type for the `listFunctions` MCP tool.
14pub(crate) struct ListFunctionsTool;
15
16impl ListFunctionsTool {
17    const NAME: &'static str = "listFunctions";
18    const TITLE: &'static str = "List Functions";
19    const DESCRIPTION: &'static str = include_str!("../../assets/tools/list_functions.md");
20}
21
22impl ToolBase for ListFunctionsTool {
23    type Parameter = ListFunctionsRequest;
24    type Output = ListFunctionsResponse;
25    type Error = ErrorData;
26
27    fn name() -> Cow<'static, str> {
28        Self::NAME.into()
29    }
30
31    fn title() -> Option<String> {
32        Some(Self::TITLE.into())
33    }
34
35    fn description() -> Option<Cow<'static, str>> {
36        Some(Self::DESCRIPTION.into())
37    }
38
39    fn annotations() -> Option<ToolAnnotations> {
40        Some(
41            ToolAnnotations::new()
42                .read_only(true)
43                .destructive(false)
44                .idempotent(true)
45                .open_world(false),
46        )
47    }
48}
49
50impl AsyncTool<PostgresHandler> for ListFunctionsTool {
51    async fn invoke(handler: &PostgresHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
52        handler.list_functions(params).await
53    }
54}
55
56/// Brief-mode SQL: `pg_proc` scan with `ILIKE` filter on function name.
57///
58/// `n.nspname = 'public'` plus `p.prokind = 'f'` keep aggregates / window
59/// functions / procedures out of the result. The `($1::text IS NULL OR ...)`
60/// trinary lets one statement cover both filtered and unfiltered cases.
61/// `(p.proname, p.oid)` is the sort key — `oid` is the unique tiebreaker
62/// across overloaded names so `OFFSET` pagination is deterministic.
63const BRIEF_SQL: &str = r"
64    SELECT p.proname
65    FROM pg_proc p
66    JOIN pg_namespace n ON n.oid = p.pronamespace
67    WHERE n.nspname = 'public'
68      AND p.prokind = 'f'
69      AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
70    ORDER BY p.proname, p.oid
71    LIMIT $2 OFFSET $3";
72
73/// Detailed-mode SQL: per-function `json_build_object` projection.
74///
75/// `n.nspname = 'public'` and `p.prokind = 'f'` filter to user-defined
76/// functions in the `public` schema. Three small lookup joins (`pg_namespace`,
77/// `pg_language`, `pg_roles`) supply the language and owner names. Postgres
78/// defers SELECT-list evaluation past `LIMIT`, so the expensive `pg_get_*`
79/// projections (`pg_get_functiondef`, `pg_get_function_arguments`,
80/// `pg_get_function_result`) and `obj_description` only run for the page's
81/// rows — never the full schema.
82///
83/// A `CROSS JOIN LATERAL` materialises `pg_get_function_arguments(p.oid)`
84/// into `args.text` so it is computed once per row and reused both in the
85/// keyed signature `name(args)` and in the JSON `arguments` field — no
86/// double-call, no Rust-side json lookup.
87///
88/// `provolatile`, `proisstrict`, `prosecdef`, `proparallel` are stable
89/// `pg_proc` columns. `provolatile` is `'i'`/`'s'`/`'v'`; `proparallel` is
90/// `'s'`/`'r'`/`'u'`; `prosecdef` is a boolean.
91const DETAILED_SQL: &str = r"
92    SELECT
93        p.proname || '(' || args.text || ')' AS name,
94        json_build_object(
95            'schema',         'public',
96            'name',           p.proname,
97            'language',       l.lanname,
98            'arguments',      args.text,
99            'returnType',     pg_get_function_result(p.oid),
100            'volatility',     CASE p.provolatile
101                                  WHEN 'i' THEN 'IMMUTABLE'
102                                  WHEN 's' THEN 'STABLE'
103                                  WHEN 'v' THEN 'VOLATILE'
104                              END,
105            'strict',         p.proisstrict,
106            'security',       CASE WHEN p.prosecdef THEN 'DEFINER' ELSE 'INVOKER' END,
107            'parallelSafety', CASE p.proparallel
108                                  WHEN 's' THEN 'SAFE'
109                                  WHEN 'r' THEN 'RESTRICTED'
110                                  WHEN 'u' THEN 'UNSAFE'
111                              END,
112            'owner',          r.rolname,
113            'description',    pg_catalog.obj_description(p.oid, 'pg_proc'),
114            'definition',     pg_get_functiondef(p.oid)
115        ) AS entry
116    FROM pg_proc p
117    JOIN pg_namespace n ON n.oid = p.pronamespace
118    JOIN pg_language  l ON l.oid = p.prolang
119    JOIN pg_roles     r ON r.oid = p.proowner
120    CROSS JOIN LATERAL (SELECT pg_get_function_arguments(p.oid) AS text) args
121    WHERE n.nspname = 'public'
122      AND p.prokind = 'f'
123      AND ($1::text IS NULL OR p.proname ILIKE '%' || $1 || '%')
124    ORDER BY p.proname, p.oid
125    LIMIT $2 OFFSET $3";
126
127impl PostgresHandler {
128    /// Lists one page of user-defined functions, optionally filtered and/or detailed.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`ErrorData`] with code `-32602` if `cursor` is malformed,
133    /// or an internal-error [`ErrorData`] if `database` is invalid
134    /// or the underlying query fails.
135    pub async fn list_functions(
136        &self,
137        ListFunctionsRequest {
138            database,
139            cursor,
140            search,
141            detailed,
142        }: ListFunctionsRequest,
143    ) -> Result<ListFunctionsResponse, ErrorData> {
144        let database = database.as_deref().map(str::trim).filter(|s| !s.is_empty());
145        let pattern = search.as_deref().map(str::trim).filter(|s| !s.is_empty());
146        let pager = Pager::new(cursor, self.config.page_size);
147
148        if detailed {
149            let rows: Vec<(String, sqlx::types::Json<serde_json::Value>)> = self
150                .connection
151                .fetch(
152                    sqlx::query(DETAILED_SQL)
153                        .bind(pattern)
154                        .bind(pager.limit())
155                        .bind(pager.offset()),
156                    database,
157                )
158                .await?;
159            let (rows, next_cursor) = pager.paginate(rows);
160            return Ok(ListFunctionsResponse::detailed(
161                rows.into_iter().map(|(key, json)| (key, json.0)).collect(),
162                next_cursor,
163            ));
164        }
165
166        let rows: Vec<String> = self
167            .connection
168            .fetch_scalar(
169                sqlx::query(BRIEF_SQL)
170                    .bind(pattern)
171                    .bind(pager.limit())
172                    .bind(pager.offset()),
173                database,
174            )
175            .await?;
176        let (functions, next_cursor) = pager.paginate(rows);
177        Ok(ListFunctionsResponse::brief(functions, next_cursor))
178    }
179}