dbmcp_postgres/
handler.rs1use dbmcp_config::DatabaseConfig;
9use dbmcp_server::{Server, server_info};
10use rmcp::RoleServer;
11use rmcp::handler::server::router::tool::ToolRouter;
12use rmcp::handler::server::tool::ToolCallContext;
13use rmcp::model::{CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, ServerInfo, Tool};
14use rmcp::service::RequestContext;
15use rmcp::{ErrorData, ServerHandler};
16
17use crate::connection::PostgresConnection;
18
19use crate::tools::{
20 CreateDatabaseTool, DropDatabaseTool, DropTableTool, ExplainQueryTool, GetTableSchemaTool, ListDatabasesTool,
21 ListFunctionsTool, ListMaterializedViewsTool, ListProceduresTool, ListTablesTool, ListTriggersTool, ListViewsTool,
22 ReadQueryTool, WriteQueryTool,
23};
24
25const DESCRIPTION: &str = "Database MCP Server for PostgreSQL";
27
28const INSTRUCTIONS: &str = r"## Workflow
30
311. Call `listDatabases` to discover available databases.
322. Call `listTables` to see tables.
333. Call `listViews` to see views in the `public` schema.
344. Call `listTriggers` to see user-defined triggers in the `public` schema.
355. Call `listFunctions` to see user-defined functions in the `public` schema.
366. Call `listProcedures` to see user-defined procedures in the `public` schema.
377. Call `listMaterializedViews` to see materialized views in the `public` schema.
388. Call `getTableSchema` to inspect columns, types, and foreign keys before writing queries.
399. Use `readQuery` for read-only SQL (SELECT, SHOW, EXPLAIN).
4010. Use `writeQuery` for data changes (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP).
4111. Use `explainQuery` to analyze query execution plans and diagnose slow queries.
4212. Use `createDatabase` to create a new database.
4313. Use `dropDatabase` to drop an existing database.
4414. Use `dropTable` to remove a table from a database (supports `cascade` for foreign key dependencies).
45
46Per-database tools default to the active database; pass `database` to target another.
47
48## Constraints
49
50- The `writeQuery`, `createDatabase`, `dropDatabase`, and `dropTable` tools are hidden when read-only mode is active.
51- Multi-statement queries are not supported. Send one statement per request.";
52
53#[derive(Clone)]
59pub struct PostgresHandler {
60 pub(crate) config: DatabaseConfig,
61 pub(crate) connection: PostgresConnection,
62 tool_router: ToolRouter<Self>,
63}
64
65impl std::fmt::Debug for PostgresHandler {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("PostgresHandler")
68 .field("read_only", &self.config.read_only)
69 .field("connection", &self.connection)
70 .finish_non_exhaustive()
71 }
72}
73
74impl PostgresHandler {
75 #[must_use]
81 pub fn new(config: &DatabaseConfig) -> Self {
82 Self {
83 config: config.clone(),
84 connection: PostgresConnection::new(config),
85 tool_router: build_tool_router(config.read_only),
86 }
87 }
88}
89
90impl From<PostgresHandler> for Server {
91 fn from(handler: PostgresHandler) -> Self {
93 Self::new(handler)
94 }
95}
96
97fn build_tool_router(read_only: bool) -> ToolRouter<PostgresHandler> {
99 let mut router = ToolRouter::new()
100 .with_async_tool::<ListDatabasesTool>()
101 .with_async_tool::<ListTablesTool>()
102 .with_async_tool::<ListViewsTool>()
103 .with_async_tool::<ListTriggersTool>()
104 .with_async_tool::<ListFunctionsTool>()
105 .with_async_tool::<ListProceduresTool>()
106 .with_async_tool::<ListMaterializedViewsTool>()
107 .with_async_tool::<GetTableSchemaTool>()
108 .with_async_tool::<ReadQueryTool>()
109 .with_async_tool::<ExplainQueryTool>();
110
111 if !read_only {
112 router = router
113 .with_async_tool::<CreateDatabaseTool>()
114 .with_async_tool::<DropDatabaseTool>()
115 .with_async_tool::<DropTableTool>()
116 .with_async_tool::<WriteQueryTool>();
117 }
118 router
119}
120
121impl ServerHandler for PostgresHandler {
122 fn get_info(&self) -> ServerInfo {
123 let mut info = server_info();
124 info.server_info.description = Some(DESCRIPTION.into());
125 info.instructions = Some(INSTRUCTIONS.into());
126 info
127 }
128
129 async fn call_tool(
130 &self,
131 request: CallToolRequestParams,
132 context: RequestContext<RoleServer>,
133 ) -> Result<CallToolResult, ErrorData> {
134 let tcc = ToolCallContext::new(self, request, context);
135 self.tool_router.call(tcc).await
136 }
137
138 async fn list_tools(
139 &self,
140 _request: Option<PaginatedRequestParams>,
141 _context: RequestContext<RoleServer>,
142 ) -> Result<ListToolsResult, ErrorData> {
143 Ok(ListToolsResult {
144 tools: self.tool_router.list_all(),
145 next_cursor: None,
146 meta: None,
147 })
148 }
149
150 fn get_tool(&self, name: &str) -> Option<Tool> {
151 self.tool_router.get(name).cloned()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use dbmcp_config::DatabaseBackend;
159
160 fn base_config() -> DatabaseConfig {
161 DatabaseConfig {
162 backend: DatabaseBackend::Postgres,
163 host: "pg.example.com".into(),
164 port: 5433,
165 user: "pgadmin".into(),
166 password: Some("pgpass".into()),
167 name: Some("mydb".into()),
168 ..DatabaseConfig::default()
169 }
170 }
171
172 fn handler(read_only: bool) -> PostgresHandler {
173 PostgresHandler::new(&DatabaseConfig {
174 read_only,
175 ..base_config()
176 })
177 }
178
179 #[tokio::test]
180 async fn handler_exposes_connection_default_db() {
181 let handler = PostgresHandler::new(&base_config());
182 assert_eq!(handler.connection.default_database_name(), "mydb");
183 }
184
185 #[tokio::test]
186 async fn router_exposes_all_fourteen_tools_in_read_write_mode() {
187 let router = handler(false).tool_router;
188 for name in [
189 "listDatabases",
190 "listTables",
191 "listViews",
192 "listTriggers",
193 "listFunctions",
194 "listProcedures",
195 "listMaterializedViews",
196 "getTableSchema",
197 "readQuery",
198 "explainQuery",
199 "createDatabase",
200 "dropDatabase",
201 "dropTable",
202 "writeQuery",
203 ] {
204 assert!(router.has_route(name), "missing tool: {name}");
205 }
206 }
207
208 #[tokio::test]
209 async fn router_hides_write_tools_in_read_only_mode() {
210 let router = handler(true).tool_router;
211 assert!(router.has_route("listDatabases"));
212 assert!(router.has_route("listTables"));
213 assert!(router.has_route("listViews"));
214 assert!(router.has_route("listTriggers"));
215 assert!(router.has_route("listFunctions"));
216 assert!(router.has_route("listProcedures"));
217 assert!(router.has_route("listMaterializedViews"));
218 assert!(router.has_route("getTableSchema"));
219 assert!(router.has_route("readQuery"));
220 assert!(router.has_route("explainQuery"));
221 assert!(!router.has_route("writeQuery"));
222 assert!(!router.has_route("createDatabase"));
223 assert!(!router.has_route("dropDatabase"));
224 assert!(!router.has_route("dropTable"));
225 }
226}