Skip to main content

database_mcp_mysql/tools/
create_database.rs

1//! MCP tool: `createDatabase`.
2
3use std::borrow::Cow;
4
5use database_mcp_server::types::{CreateDatabaseRequest, MessageResponse};
6use database_mcp_sql::Connection as _;
7use database_mcp_sql::SqlError;
8use database_mcp_sql::sanitize::{quote_ident, quote_literal, validate_ident};
9use rmcp::handler::server::router::tool::{AsyncTool, ToolBase};
10use rmcp::model::{ErrorData, ToolAnnotations};
11use sqlparser::dialect::MySqlDialect;
12
13use crate::MysqlHandler;
14
15/// Marker type for the `createDatabase` MCP tool.
16pub(crate) struct CreateDatabaseTool;
17
18impl CreateDatabaseTool {
19    const NAME: &'static str = "createDatabase";
20    const TITLE: &'static str = "Create Database";
21    const DESCRIPTION: &'static str = r#"Create a new database on the connected server.
22
23<usecase>
24Use when:
25- Setting up a new database for a project or application
26- The user asks to create a database
27</usecase>
28
29<examples>
30✓ "Create a database called analytics" → createDatabase(database="analytics")
31✗ "Create a table" → use writeQuery with CREATE TABLE
32</examples>
33
34<important>
35Database names must contain only alphanumeric characters and underscores.
36If the database already exists, returns a message indicating so without error.
37</important>
38
39<what_it_returns>
40A confirmation message with the created database name.
41</what_it_returns>"#;
42}
43
44impl ToolBase for CreateDatabaseTool {
45    type Parameter = CreateDatabaseRequest;
46    type Output = MessageResponse;
47    type Error = ErrorData;
48
49    fn name() -> Cow<'static, str> {
50        Self::NAME.into()
51    }
52
53    fn title() -> Option<String> {
54        Some(Self::TITLE.into())
55    }
56
57    fn description() -> Option<Cow<'static, str>> {
58        Some(Self::DESCRIPTION.into())
59    }
60
61    fn annotations() -> Option<ToolAnnotations> {
62        Some(
63            ToolAnnotations::new()
64                .read_only(false)
65                .destructive(false)
66                .idempotent(false)
67                .open_world(false),
68        )
69    }
70}
71
72impl AsyncTool<MysqlHandler> for CreateDatabaseTool {
73    async fn invoke(handler: &MysqlHandler, params: Self::Parameter) -> Result<Self::Output, Self::Error> {
74        Ok(handler.create_database(params).await?)
75    }
76}
77
78impl MysqlHandler {
79    /// Creates a database if it doesn't exist.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`SqlError`] if read-only or the query fails.
84    pub async fn create_database(
85        &self,
86        CreateDatabaseRequest { database }: CreateDatabaseRequest,
87    ) -> Result<MessageResponse, SqlError> {
88        if self.config.read_only {
89            return Err(SqlError::ReadOnlyViolation);
90        }
91
92        validate_ident(&database)?;
93
94        let check_sql = format!(
95            r"
96            SELECT CAST(SCHEMA_NAME AS CHAR)
97            FROM information_schema.SCHEMATA
98            WHERE SCHEMA_NAME = {}",
99            quote_literal(&database),
100        );
101
102        let exists: Option<String> = self.connection.fetch_optional(check_sql.as_str(), None).await?;
103
104        if exists.is_some() {
105            return Ok(MessageResponse {
106                message: format!("Database '{database}' already exists."),
107            });
108        }
109
110        let create_sql = format!(
111            "CREATE DATABASE IF NOT EXISTS {}",
112            quote_ident(&database, &MySqlDialect {})
113        );
114
115        self.connection.execute(create_sql.as_str(), None).await?;
116
117        Ok(MessageResponse {
118            message: format!("Database '{database}' created successfully."),
119        })
120    }
121}