Skip to main content

database_mcp_mysql/
tools.rs

1//! MCP tool definitions for the MySQL/MariaDB backend.
2//!
3//! Uses rmcp `#[tool]` attribute macros to define tools as methods
4//! on [`MysqlAdapter`], eliminating manual [`ToolBase`] and
5//! [`AsyncTool`] implementations.
6
7use database_mcp_server::types::{CreateDatabaseRequest, GetTableSchemaRequest, ListTablesRequest, QueryRequest};
8use rmcp::handler::server::tool::ToolRouter;
9use rmcp::handler::server::wrapper::Parameters;
10use rmcp::model::{CallToolResult, Content, ErrorData};
11use rmcp::tool;
12
13use database_mcp_sql::validation::validate_read_only_with_dialect;
14
15use super::MysqlAdapter;
16
17impl MysqlAdapter {
18    /// Names of tools that require write access.
19    const WRITE_TOOL_NAMES: &[&str] = &["write_query", "create_database"];
20
21    /// Builds the tool router, excluding write tools in read-only mode.
22    #[must_use]
23    pub fn build_tool_router(&self) -> ToolRouter<Self> {
24        let mut router = Self::tool_router();
25        if self.config.read_only {
26            for name in Self::WRITE_TOOL_NAMES {
27                router.remove_route(name);
28            }
29        }
30        router
31    }
32}
33
34#[rmcp::tool_router]
35impl MysqlAdapter {
36    /// List all accessible databases on the connected database server.
37    /// Call this first to discover available database names.
38    #[tool(
39        name = "list_databases",
40        annotations(
41            read_only_hint = true,
42            destructive_hint = false,
43            idempotent_hint = true,
44            open_world_hint = false
45        )
46    )]
47    pub async fn tool_list_databases(&self) -> Result<CallToolResult, ErrorData> {
48        let result = self.list_databases().await?;
49        Ok(CallToolResult::success(vec![Content::json(result)?]))
50    }
51
52    /// List all tables in a specific database.
53    /// Requires `database_name` from `list_databases`.
54    #[tool(
55        name = "list_tables",
56        annotations(
57            read_only_hint = true,
58            destructive_hint = false,
59            idempotent_hint = true,
60            open_world_hint = false
61        )
62    )]
63    pub async fn tool_list_tables(
64        &self,
65        Parameters(request): Parameters<ListTablesRequest>,
66    ) -> Result<CallToolResult, ErrorData> {
67        let result = self.list_tables(&request.database_name).await?;
68        Ok(CallToolResult::success(vec![Content::json(result)?]))
69    }
70
71    /// Get column definitions (type, nullable, key, default) and foreign key
72    /// relationships for a table. Requires `database_name` and `table_name`.
73    #[tool(
74        name = "get_table_schema",
75        annotations(
76            read_only_hint = true,
77            destructive_hint = false,
78            idempotent_hint = true,
79            open_world_hint = false
80        )
81    )]
82    pub async fn tool_get_table_schema(
83        &self,
84        Parameters(request): Parameters<GetTableSchemaRequest>,
85    ) -> Result<CallToolResult, ErrorData> {
86        let result = self
87            .get_table_schema(&request.database_name, &request.table_name)
88            .await?;
89        Ok(CallToolResult::success(vec![Content::json(result)?]))
90    }
91
92    /// Execute a read-only SQL query (SELECT, SHOW, DESCRIBE, USE, EXPLAIN).
93    #[tool(
94        name = "read_query",
95        annotations(
96            read_only_hint = true,
97            destructive_hint = false,
98            idempotent_hint = true,
99            open_world_hint = true
100        )
101    )]
102    pub async fn tool_read_query(
103        &self,
104        Parameters(request): Parameters<QueryRequest>,
105    ) -> Result<CallToolResult, ErrorData> {
106        validate_read_only_with_dialect(&request.query, &sqlparser::dialect::MySqlDialect {})?;
107
108        let db = Some(request.database_name.trim()).filter(|s| !s.is_empty());
109        let result = self.execute_query(&request.query, db).await?;
110        Ok(CallToolResult::success(vec![Content::json(result)?]))
111    }
112
113    /// Execute a write SQL query (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP).
114    #[tool(
115        name = "write_query",
116        annotations(
117            read_only_hint = false,
118            destructive_hint = true,
119            idempotent_hint = false,
120            open_world_hint = true
121        )
122    )]
123    pub async fn tool_write_query(
124        &self,
125        Parameters(request): Parameters<QueryRequest>,
126    ) -> Result<CallToolResult, ErrorData> {
127        let db = Some(request.database_name.trim()).filter(|s| !s.is_empty());
128        let result = self.execute_query(&request.query, db).await?;
129        Ok(CallToolResult::success(vec![Content::json(result)?]))
130    }
131
132    /// Create a new database. Not supported for `SQLite`.
133    #[tool(
134        name = "create_database",
135        annotations(
136            read_only_hint = false,
137            destructive_hint = false,
138            idempotent_hint = false,
139            open_world_hint = false
140        )
141    )]
142    pub async fn tool_create_database(
143        &self,
144        Parameters(request): Parameters<CreateDatabaseRequest>,
145    ) -> Result<CallToolResult, ErrorData> {
146        let result = self.create_database(&request.database_name).await?;
147        Ok(CallToolResult::success(vec![Content::json(result)?]))
148    }
149}