Skip to main content

database_mcp_postgres/
tools.rs

1//! MCP tool definitions for the `PostgreSQL` backend.
2//!
3//! Uses rmcp `#[tool]` attribute macros to define tools as methods
4//! on [`PostgresAdapter`], eliminating manual [`ToolBase`] and
5//! [`AsyncTool`] implementations.
6
7use super::types::DropTableRequest;
8use database_mcp_server::types::{
9    CreateDatabaseRequest, DropDatabaseRequest, ExplainQueryRequest, GetTableSchemaRequest, ListDatabasesResponse,
10    ListTablesRequest, ListTablesResponse, MessageResponse, QueryRequest, QueryResponse, TableSchemaResponse,
11};
12use rmcp::handler::server::tool::ToolRouter;
13use rmcp::handler::server::wrapper::{Json, Parameters};
14use rmcp::model::ErrorData;
15use rmcp::tool;
16
17use super::PostgresAdapter;
18
19impl PostgresAdapter {
20    /// Names of tools that require write access.
21    const WRITE_TOOL_NAMES: &[&str] = &["write_query", "create_database", "drop_database", "drop_table"];
22
23    /// Builds the tool router, excluding write tools in read-only mode.
24    #[must_use]
25    pub fn build_tool_router(&self) -> ToolRouter<Self> {
26        let mut router = Self::tool_router();
27        if self.config.read_only {
28            for name in Self::WRITE_TOOL_NAMES {
29                router.remove_route(name);
30            }
31        }
32        router
33    }
34}
35
36#[rmcp::tool_router]
37impl PostgresAdapter {
38    /// List all accessible databases on the connected database server.
39    /// Call this first to discover available database names.
40    #[tool(
41        name = "list_databases",
42        annotations(
43            read_only_hint = true,
44            destructive_hint = false,
45            idempotent_hint = true,
46            open_world_hint = false
47        )
48    )]
49    pub async fn tool_list_databases(&self) -> Result<Json<ListDatabasesResponse>, ErrorData> {
50        Ok(Json(self.list_databases().await?))
51    }
52
53    /// Create a new database.
54    #[tool(
55        name = "create_database",
56        annotations(
57            read_only_hint = false,
58            destructive_hint = false,
59            idempotent_hint = false,
60            open_world_hint = false
61        )
62    )]
63    pub async fn tool_create_database(
64        &self,
65        Parameters(request): Parameters<CreateDatabaseRequest>,
66    ) -> Result<Json<MessageResponse>, ErrorData> {
67        Ok(Json(self.create_database(&request).await?))
68    }
69
70    /// Drop an existing database. Cannot drop the currently connected database.
71    #[tool(
72        name = "drop_database",
73        annotations(
74            read_only_hint = false,
75            destructive_hint = true,
76            idempotent_hint = false,
77            open_world_hint = false
78        )
79    )]
80    pub async fn tool_drop_database(
81        &self,
82        Parameters(request): Parameters<DropDatabaseRequest>,
83    ) -> Result<Json<MessageResponse>, ErrorData> {
84        Ok(Json(self.drop_database(&request).await?))
85    }
86
87    /// List all tables in a specific database.
88    /// Requires `database_name` from `list_databases`.
89    #[tool(
90        name = "list_tables",
91        annotations(
92            read_only_hint = true,
93            destructive_hint = false,
94            idempotent_hint = true,
95            open_world_hint = false
96        )
97    )]
98    pub async fn tool_list_tables(
99        &self,
100        Parameters(request): Parameters<ListTablesRequest>,
101    ) -> Result<Json<ListTablesResponse>, ErrorData> {
102        Ok(Json(self.list_tables(&request).await?))
103    }
104
105    /// Get column definitions (type, nullable, key, default) and foreign key
106    /// relationships for a table. Requires `database_name` and `table_name`.
107    #[tool(
108        name = "get_table_schema",
109        annotations(
110            read_only_hint = true,
111            destructive_hint = false,
112            idempotent_hint = true,
113            open_world_hint = false
114        )
115    )]
116    pub async fn tool_get_table_schema(
117        &self,
118        Parameters(request): Parameters<GetTableSchemaRequest>,
119    ) -> Result<Json<TableSchemaResponse>, ErrorData> {
120        Ok(Json(self.get_table_schema(&request).await?))
121    }
122
123    /// Drop a table from a database. Checks for foreign key dependencies
124    /// via the database engine — use `cascade` to force on `PostgreSQL`.
125    #[tool(
126        name = "drop_table",
127        annotations(
128            read_only_hint = false,
129            destructive_hint = true,
130            idempotent_hint = false,
131            open_world_hint = false
132        )
133    )]
134    pub async fn tool_drop_table(
135        &self,
136        Parameters(request): Parameters<DropTableRequest>,
137    ) -> Result<Json<MessageResponse>, ErrorData> {
138        Ok(Json(self.drop_table(&request).await?))
139    }
140
141    /// Execute a read-only SQL query (SELECT, SHOW, DESCRIBE, USE, EXPLAIN).
142    #[tool(
143        name = "read_query",
144        annotations(
145            read_only_hint = true,
146            destructive_hint = false,
147            idempotent_hint = true,
148            open_world_hint = true
149        )
150    )]
151    pub async fn tool_read_query(
152        &self,
153        Parameters(request): Parameters<QueryRequest>,
154    ) -> Result<Json<QueryResponse>, ErrorData> {
155        Ok(Json(self.read_query(&request).await?))
156    }
157
158    /// Execute a write SQL query (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP).
159    #[tool(
160        name = "write_query",
161        annotations(
162            read_only_hint = false,
163            destructive_hint = true,
164            idempotent_hint = false,
165            open_world_hint = true
166        )
167    )]
168    pub async fn tool_write_query(
169        &self,
170        Parameters(request): Parameters<QueryRequest>,
171    ) -> Result<Json<QueryResponse>, ErrorData> {
172        Ok(Json(self.write_query(&request).await?))
173    }
174
175    /// Return the execution plan for a SQL query.
176    #[tool(
177        name = "explain_query",
178        annotations(
179            read_only_hint = true,
180            destructive_hint = false,
181            idempotent_hint = true,
182            open_world_hint = true
183        )
184    )]
185    pub async fn tool_explain_query(
186        &self,
187        Parameters(request): Parameters<ExplainQueryRequest>,
188    ) -> Result<Json<QueryResponse>, ErrorData> {
189        Ok(Json(self.explain_query(&request).await?))
190    }
191}