use crate::{BodyType, GraphMailClient, Message, MessageBody, Recipient, SendMailRequest};
use rmcp::model::*;
use rmcp::{ServerHandler, ServiceExt};
use schemars::JsonSchema;
use serde::Deserialize;
use std::sync::Arc;
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct SendEmailParams {
#[schemars(
description = "User id or userPrincipalName to send as (e.g. user@tenant.onmicrosoft.com)"
)]
pub from_user: String,
#[schemars(description = "Recipient email addresses (to)")]
pub to: Vec<String>,
#[schemars(description = "Subject of the email")]
pub subject: String,
#[schemars(description = "Body content (plain text or HTML depending on body_type)")]
pub body: String,
#[schemars(description = "Body format: \"text\" or \"html\". Default \"text\"")]
#[serde(default)]
pub body_type: String,
#[schemars(description = "CC recipient email addresses. Optional")]
#[serde(default)]
pub cc: Vec<String>,
#[schemars(description = "BCC recipient email addresses. Optional")]
#[serde(default)]
pub bcc: Vec<String>,
}
#[derive(Clone)]
pub struct WangaMailMcpServer {
client: Arc<GraphMailClient>,
}
impl WangaMailMcpServer {
pub fn new(client: GraphMailClient) -> Self {
Self {
client: Arc::new(client),
}
}
pub fn from_env() -> crate::Result<Self> {
let tenant_id = std::env::var("AZURE_TENANT_ID")
.map_err(|_| crate::Error::Config("AZURE_TENANT_ID not set".into()))?;
let client_id = std::env::var("AZURE_CLIENT_ID")
.map_err(|_| crate::Error::Config("AZURE_CLIENT_ID not set".into()))?;
let client_secret = std::env::var("AZURE_CLIENT_SECRET")
.map_err(|_| crate::Error::Config("AZURE_CLIENT_SECRET not set".into()))?;
let client = GraphMailClient::builder()
.tenant_id(tenant_id)
.client_id(client_id)
.client_secret(client_secret)
.build()?;
Ok(Self::new(client))
}
pub async fn run_stdio(
self,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let transport = (tokio::io::stdin(), tokio::io::stdout());
let service = self
.serve(transport)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.to_string().into() })?;
service
.waiting()
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { e.to_string().into() })?;
Ok(())
}
fn send_email_tool() -> Tool {
Tool::new(
"send_email",
"Send an email via Microsoft Graph on behalf of a Microsoft tenant user. Requires Azure app registration with Mail.Send application permission.",
Arc::new(serde_json::Map::new()),
)
.with_input_schema::<SendEmailParams>()
}
}
impl ServerHandler for WangaMailMcpServer {
fn get_info(&self) -> ServerInfo {
InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
}
fn get_tool(&self, name: &str) -> Option<Tool> {
if name == "send_email" {
Some(Self::send_email_tool())
} else {
None
}
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<ListToolsResult, ErrorData> {
Ok(ListToolsResult {
tools: vec![Self::send_email_tool()],
next_cursor: None,
meta: None,
})
}
async fn call_tool(
&self,
request: CallToolRequestParams,
_context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<CallToolResult, ErrorData> {
if request.name != "send_email" {
return Err(ErrorData::invalid_params(
"unknown_tool",
Some(serde_json::json!({ "name": request.name })),
));
}
let args_value = request
.arguments
.map(serde_json::Value::Object)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
let params: SendEmailParams = serde_json::from_value(args_value).map_err(|err| {
ErrorData::invalid_params(
"invalid_arguments",
Some(serde_json::json!({ "error": err.to_string() })),
)
})?;
let body_type = if params.body_type.eq_ignore_ascii_case("html") {
BodyType::HTML
} else {
BodyType::Text
};
let to_recipients: Vec<Recipient> = params
.to
.iter()
.map(|addr| Recipient::new(addr.as_str()))
.collect();
let cc_recipients: Vec<Recipient> = params
.cc
.iter()
.map(|addr| Recipient::new(addr.as_str()))
.collect();
let bcc_recipients: Vec<Recipient> = params
.bcc
.iter()
.map(|addr| Recipient::new(addr.as_str()))
.collect();
let message = Message {
subject: params.subject,
body: MessageBody {
content_type: body_type,
content: params.body,
},
to_recipients,
cc_recipients,
bcc_recipients,
..Default::default()
};
let req = SendMailRequest::new(message);
match self.client.send_mail(¶ms.from_user, req).await {
Ok(()) => Ok(CallToolResult::success(vec![Content {
raw: RawContent::Text(RawTextContent {
text: "Email sent successfully.".to_string(),
meta: None,
}),
annotations: None,
}])),
Err(e) => Ok(CallToolResult::error(vec![Content {
raw: RawContent::Text(RawTextContent {
text: format!("Send failed: {e}"),
meta: None,
}),
annotations: None,
}])),
}
}
}