use std::sync::Arc;
use rmcp::handler::server::tool::ToolRouter;
use rmcp::model::*;
use rmcp::schemars;
use rmcp::schemars::JsonSchema;
use rmcp::{ServerHandler, handler::server::wrapper::Parameters, tool, tool_handler, tool_router};
use serde::Deserialize;
use crate::client::AscClient;
#[derive(Clone)]
pub struct AscMcpServer {
client: Arc<AscClient>,
tool_router: ToolRouter<Self>,
}
impl AscMcpServer {
pub fn new(client: Arc<AscClient>) -> Self {
Self {
client,
tool_router: Self::tool_router(),
}
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ProductIdParam {
pub product_id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct WorkflowIdParam {
pub workflow_id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct BuildRunIdParam {
pub build_run_id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct StartBuildParam {
pub workflow_id: String,
pub git_reference_id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct AppIdParam {
pub app_id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SalesReportParam {
pub vendor_number: String,
pub report_type: String,
pub report_sub_type: String,
pub frequency: String,
pub report_date: String,
}
fn json_result<T: serde::Serialize>(value: &T) -> Result<CallToolResult, rmcp::ErrorData> {
let text = serde_json::to_string_pretty(value)
.map_err(|e| rmcp::ErrorData::internal_error(format!("serialization error: {e}"), None))?;
Ok(CallToolResult::success(vec![Content::text(text)]))
}
fn api_err(e: crate::client::ApiError) -> rmcp::ErrorData {
rmcp::ErrorData::internal_error(e.to_string(), None)
}
#[tool_router]
impl AscMcpServer {
#[tool(description = "List all Xcode Cloud CI products")]
async fn list_products(&self) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self.client.list_products().await.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "Get details of a specific CI product")]
async fn get_product(
&self,
params: Parameters<ProductIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.get_product(¶ms.0.product_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "List workflows for a CI product")]
async fn list_workflows(
&self,
params: Parameters<ProductIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.list_workflows(¶ms.0.product_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "List build runs for a workflow")]
async fn list_build_runs(
&self,
params: Parameters<WorkflowIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.list_build_runs(¶ms.0.workflow_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "Get details of a specific build run")]
async fn get_build_run(
&self,
params: Parameters<BuildRunIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.get_build_run(¶ms.0.build_run_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "Start a new build for a workflow")]
async fn start_build(
&self,
params: Parameters<StartBuildParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.start_build(¶ms.0.workflow_id, ¶ms.0.git_reference_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "List actions in a build run")]
async fn list_build_actions(
&self,
params: Parameters<BuildRunIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.list_build_actions(¶ms.0.build_run_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "List all apps in App Store Connect")]
async fn list_apps(&self) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self.client.list_apps().await.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "Get details of a specific app")]
async fn get_app(
&self,
params: Parameters<AppIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.get_app(¶ms.0.app_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "List customer reviews for an app")]
async fn list_customer_reviews(
&self,
params: Parameters<AppIdParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let resp = self
.client
.list_customer_reviews(¶ms.0.app_id)
.await
.map_err(api_err)?;
json_result(&resp)
}
#[tool(description = "Download and parse a sales report")]
async fn get_sales_report(
&self,
params: Parameters<SalesReportParam>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let p = ¶ms.0;
let rows = self
.client
.get_sales_report(
&p.vendor_number,
&p.report_type,
&p.report_sub_type,
&p.frequency,
&p.report_date,
)
.await
.map_err(api_err)?;
json_result(&rows)
}
}
#[tool_handler]
impl ServerHandler for AscMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_tools_with(ToolsCapability { list_changed: None })
.build(),
)
.with_server_info(Implementation::new("asc-mcp", env!("CARGO_PKG_VERSION")))
.with_protocol_version(ProtocolVersion::V_2024_11_05)
.with_instructions(
"MCP server for Apple App Store Connect API. \
Provides tools for Xcode Cloud CI (products, workflows, build runs, actions), \
App Store apps, customer reviews, and sales reports.",
)
}
}
#[cfg(test)]
#[path = "tools_tests.rs"]
mod tests;