use rmcp::handler::server::wrapper::Json;
use rmcp::{ErrorData, elicit_safe};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub type ToolResult<T> = Result<Json<T>, ErrorData>;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PingResult {
#[schemars(description = "Connection status: ok or error")]
pub status: String,
#[schemars(description = "Query latency in milliseconds")]
pub latency_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TableInfo {
#[schemars(description = "Table name")]
pub name: String,
#[schemars(description = "Table type: TABLE, VIEW, SYNONYM, etc.")]
pub table_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ColumnInfo {
#[schemars(description = "Column name")]
pub name: String,
#[schemars(description = "HANA data type: VARCHAR, INTEGER, DECIMAL, TIMESTAMP, etc.")]
pub data_type: String,
#[schemars(description = "Whether column accepts NULL values")]
pub nullable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TableSchema {
#[schemars(description = "Table name")]
pub table_name: String,
#[schemars(description = "List of column definitions")]
pub columns: Vec<ColumnInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct QueryResult {
#[schemars(description = "Column names in result set")]
pub columns: Vec<String>,
#[schemars(description = "Result rows as JSON arrays")]
pub rows: Vec<Vec<serde_json::Value>>,
#[schemars(description = "Number of rows returned")]
pub row_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(description = "Schema name")]
pub struct SchemaName {
#[schemars(description = "Name of the schema")]
pub name: String,
}
elicit_safe!(SchemaName);
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListTablesParams {
#[serde(default)]
#[schemars(
description = "Schema name to filter tables. Leave empty to use CURRENT_SCHEMA (default behavior). Example: 'SYSTEM', 'MY_SCHEMA'"
)]
pub schema: Option<SchemaName>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DescribeTableParams {
#[schemars(description = "Name of the table to describe. Example: 'EMPLOYEES', 'ORDERS'")]
pub table: String,
#[serde(default)]
#[schemars(
description = "Schema name where the table is located. Leave empty to use CURRENT_SCHEMA. Example: 'SYSTEM', 'MY_SCHEMA'"
)]
pub schema: Option<SchemaName>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ExecuteSqlParams {
#[schemars(
description = "SQL query to execute. In read-only mode, only SELECT, WITH, EXPLAIN, and CALL are allowed"
)]
pub sql: String,
#[serde(default)]
#[schemars(description = "Optional row limit. Server may enforce maximum limit")]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ExecuteDmlParams {
#[schemars(description = "SQL DML statement. Allowed: INSERT, UPDATE, DELETE")]
pub sql: String,
#[serde(default)]
#[schemars(description = "Schema name. Leave empty to use CURRENT_SCHEMA")]
pub schema: Option<SchemaName>,
#[serde(default)]
#[schemars(description = "Skip confirmation prompt (use with caution)")]
pub force: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DmlResult {
#[schemars(description = "Operation type: INSERT, UPDATE, or DELETE")]
pub operation: String,
#[schemars(description = "Number of rows inserted, updated, or deleted")]
pub affected_rows: u64,
#[schemars(description = "Status: success or error")]
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Additional message or warning")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(description = "Confirm DML operation execution")]
pub struct DmlConfirmation {
#[schemars(description = "Type 'yes' or 'confirm' to proceed")]
pub confirm: String,
}
elicit_safe!(DmlConfirmation);
impl DmlConfirmation {
#[must_use]
pub fn is_confirmed(&self) -> bool {
let normalized = self.confirm.trim().to_lowercase();
matches!(normalized.as_str(), "yes" | "y" | "confirm" | "ok" | "true")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "UPPERCASE")]
pub enum ParameterDirection {
In,
Out,
InOut,
}
impl ParameterDirection {
#[must_use]
pub fn from_hana_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"IN" => Some(Self::In),
"OUT" => Some(Self::Out),
"INOUT" => Some(Self::InOut),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProcedureParameter {
#[schemars(description = "Parameter name")]
pub name: String,
#[schemars(description = "Parameter position (1-indexed)")]
pub position: u32,
#[schemars(description = "HANA data type")]
pub data_type: String,
#[schemars(description = "Parameter direction: IN, OUT, or INOUT")]
pub direction: ParameterDirection,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Length for string/binary types")]
pub length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Precision for numeric types")]
pub precision: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Scale for numeric types")]
pub scale: Option<u32>,
#[schemars(description = "Whether parameter has a default value")]
pub has_default: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProcedureInfo {
#[schemars(description = "Procedure name")]
pub name: String,
#[schemars(description = "Schema name")]
pub schema: String,
#[schemars(description = "Procedure type: PROCEDURE or FUNCTION")]
pub procedure_type: String,
#[schemars(description = "Whether procedure only reads data")]
pub is_read_only: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProcedureSchema {
#[schemars(description = "Procedure name")]
pub name: String,
#[schemars(description = "Schema name")]
pub schema: String,
#[schemars(description = "List of procedure parameters")]
pub parameters: Vec<ProcedureParameter>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Number of result sets returned")]
pub result_set_count: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ListProceduresParams {
#[serde(default)]
#[schemars(description = "Schema name to filter procedures. Leave empty for CURRENT_SCHEMA")]
pub schema: Option<SchemaName>,
#[serde(default)]
#[schemars(description = "Filter by procedure name pattern (SQL LIKE syntax, e.g., 'GET_%')")]
pub name_pattern: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DescribeProcedureParams {
#[schemars(description = "Procedure name to describe")]
pub procedure: String,
#[serde(default)]
#[schemars(description = "Schema name. Leave empty for CURRENT_SCHEMA")]
pub schema: Option<SchemaName>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CallProcedureParams {
#[schemars(description = "Procedure name. Format: SCHEMA.PROCEDURE or PROCEDURE")]
pub procedure: String,
#[serde(default)]
#[schemars(description = "Input parameters as JSON object")]
pub parameters: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(default)]
#[schemars(description = "Schema name. Leave empty to use CURRENT_SCHEMA")]
pub schema: Option<SchemaName>,
#[serde(default)]
#[schemars(description = "Use explicit transaction control")]
pub explicit_transaction: bool,
#[serde(default)]
#[schemars(description = "Skip confirmation prompt (use with caution)")]
pub force: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProcedureResultSet {
#[schemars(description = "Result set index (0-based)")]
pub index: usize,
#[schemars(description = "Column names in result set")]
pub columns: Vec<String>,
#[schemars(description = "Result rows as JSON arrays")]
pub rows: Vec<Vec<serde_json::Value>>,
#[schemars(description = "Number of rows returned")]
pub row_count: usize,
#[serde(default)]
#[schemars(description = "Whether result was truncated due to row limit")]
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OutputParameter {
#[schemars(description = "Parameter name")]
pub name: String,
#[schemars(description = "Output parameter value")]
pub value: serde_json::Value,
#[schemars(description = "HANA data type")]
pub data_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProcedureResult {
#[schemars(description = "Executed procedure name")]
pub procedure: String,
#[schemars(description = "Status: success or error")]
pub status: String,
#[schemars(description = "Result sets from procedure")]
pub result_sets: Vec<ProcedureResultSet>,
#[schemars(description = "Output parameter values")]
pub output_parameters: Vec<OutputParameter>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Number of rows affected by DML operations")]
pub affected_rows: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(description = "Additional execution message")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(description = "Confirm stored procedure execution")]
pub struct ProcedureConfirmation {
#[schemars(description = "Type 'yes' or 'confirm' to proceed")]
pub confirm: String,
}
elicit_safe!(ProcedureConfirmation);
impl ProcedureConfirmation {
#[must_use]
pub fn is_confirmed(&self) -> bool {
let normalized = self.confirm.trim().to_lowercase();
matches!(normalized.as_str(), "yes" | "y" | "confirm" | "ok" | "true")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dml_confirmation_is_confirmed() {
assert!(
DmlConfirmation {
confirm: "yes".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "YES".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "y".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "Y".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "confirm".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "CONFIRM".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "ok".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "OK".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: "true".to_string()
}
.is_confirmed()
);
assert!(
DmlConfirmation {
confirm: " yes ".to_string()
}
.is_confirmed()
);
}
#[test]
fn test_dml_confirmation_not_confirmed() {
assert!(
!DmlConfirmation {
confirm: "no".to_string()
}
.is_confirmed()
);
assert!(
!DmlConfirmation {
confirm: "false".to_string()
}
.is_confirmed()
);
assert!(
!DmlConfirmation {
confirm: "cancel".to_string()
}
.is_confirmed()
);
assert!(
!DmlConfirmation {
confirm: "".to_string()
}
.is_confirmed()
);
assert!(
!DmlConfirmation {
confirm: "n".to_string()
}
.is_confirmed()
);
}
#[test]
fn test_dml_result_serialization() {
let result = DmlResult {
operation: "INSERT".to_string(),
affected_rows: 5,
status: "success".to_string(),
message: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("INSERT"));
assert!(json.contains("5"));
assert!(json.contains("success"));
assert!(!json.contains("message"));
let result_with_message = DmlResult {
operation: "DELETE".to_string(),
affected_rows: 100,
status: "success".to_string(),
message: Some("Deleted old records".to_string()),
};
let json = serde_json::to_string(&result_with_message).unwrap();
assert!(json.contains("message"));
assert!(json.contains("Deleted old records"));
}
#[test]
fn test_execute_dml_params_deserialization() {
let json = r#"{
"sql": "INSERT INTO users VALUES (1, 'test')",
"schema": {"name": "APP"}
}"#;
let params: ExecuteDmlParams = serde_json::from_str(json).unwrap();
assert_eq!(params.sql, "INSERT INTO users VALUES (1, 'test')");
assert!(params.schema.is_some());
assert_eq!(params.schema.unwrap().name, "APP");
assert!(!params.force);
}
#[test]
fn test_execute_dml_params_with_force() {
let json = r#"{
"sql": "DELETE FROM logs WHERE created_at < '2024-01-01'",
"force": true
}"#;
let params: ExecuteDmlParams = serde_json::from_str(json).unwrap();
assert!(params.force);
assert!(params.schema.is_none());
}
#[test]
fn test_parameter_direction_from_hana_str() {
assert_eq!(
ParameterDirection::from_hana_str("IN"),
Some(ParameterDirection::In)
);
assert_eq!(
ParameterDirection::from_hana_str("OUT"),
Some(ParameterDirection::Out)
);
assert_eq!(
ParameterDirection::from_hana_str("INOUT"),
Some(ParameterDirection::InOut)
);
assert_eq!(
ParameterDirection::from_hana_str("in"),
Some(ParameterDirection::In)
);
assert_eq!(ParameterDirection::from_hana_str("INVALID"), None);
}
#[test]
fn test_parameter_direction_serialization() {
assert_eq!(
serde_json::to_string(&ParameterDirection::In).unwrap(),
r#""IN""#
);
assert_eq!(
serde_json::to_string(&ParameterDirection::Out).unwrap(),
r#""OUT""#
);
assert_eq!(
serde_json::to_string(&ParameterDirection::InOut).unwrap(),
r#""INOUT""#
);
}
#[test]
fn test_procedure_confirmation_is_confirmed() {
assert!(
ProcedureConfirmation {
confirm: "yes".to_string()
}
.is_confirmed()
);
assert!(
ProcedureConfirmation {
confirm: "YES".to_string()
}
.is_confirmed()
);
assert!(
ProcedureConfirmation {
confirm: "y".to_string()
}
.is_confirmed()
);
assert!(
ProcedureConfirmation {
confirm: "confirm".to_string()
}
.is_confirmed()
);
assert!(
ProcedureConfirmation {
confirm: " ok ".to_string()
}
.is_confirmed()
);
}
#[test]
fn test_procedure_confirmation_not_confirmed() {
assert!(
!ProcedureConfirmation {
confirm: "no".to_string()
}
.is_confirmed()
);
assert!(
!ProcedureConfirmation {
confirm: "cancel".to_string()
}
.is_confirmed()
);
assert!(
!ProcedureConfirmation {
confirm: "".to_string()
}
.is_confirmed()
);
}
#[test]
fn test_call_procedure_params_deserialization() {
let json = r#"{
"procedure": "GET_USER",
"parameters": {"USER_ID": 123},
"schema": {"name": "APP"}
}"#;
let params: CallProcedureParams = serde_json::from_str(json).unwrap();
assert_eq!(params.procedure, "GET_USER");
assert!(params.parameters.is_some());
let params_map = params.parameters.unwrap();
assert_eq!(params_map.get("USER_ID").unwrap(), &serde_json::json!(123));
assert_eq!(params.schema.unwrap().name, "APP");
assert!(!params.explicit_transaction);
assert!(!params.force);
}
#[test]
fn test_procedure_result_serialization() {
let result = ProcedureResult {
procedure: "GET_USER".to_string(),
status: "success".to_string(),
result_sets: vec![ProcedureResultSet {
index: 0,
columns: vec!["ID".to_string(), "NAME".to_string()],
rows: vec![vec![serde_json::json!(1), serde_json::json!("Alice")]],
row_count: 1,
truncated: false,
}],
output_parameters: vec![],
affected_rows: None,
message: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("GET_USER"));
assert!(json.contains("success"));
assert!(json.contains("Alice"));
assert!(!json.contains("affected_rows"));
assert!(!json.contains("message"));
}
#[test]
fn test_procedure_info_serialization() {
let info = ProcedureInfo {
name: "MY_PROC".to_string(),
schema: "APP".to_string(),
procedure_type: "PROCEDURE".to_string(),
is_read_only: true,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("MY_PROC"));
assert!(json.contains("APP"));
assert!(json.contains("PROCEDURE"));
assert!(json.contains("true"));
}
}