use {
crate::{account::lib::is_logged_in, client, token::get_smb_token::get_smb_token},
anyhow::{anyhow, Result},
rmcp::{
handler::server::{wrapper::Parameters, ServerHandler},
model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo},
tool, tool_handler, tool_router,
transport::stdio,
ErrorData, ServiceExt,
},
schemars::JsonSchema,
serde::Deserialize,
smbcloud_auth::me::me,
smbcloud_model::project::ProjectCreate,
smbcloud_network::environment::Environment,
smbcloud_networking_project::{
crud_project_create::create_project, crud_project_delete::delete_project,
crud_project_deployment_read::get_deployments, crud_project_read::get_project,
crud_project_read::get_projects, crud_project_update::update_project,
},
};
pub struct SmbMcpServer {
environment: Environment,
}
impl SmbMcpServer {
pub fn new(environment: Environment) -> Self {
Self { environment }
}
fn access_token(&self) -> Result<String, ErrorData> {
if !is_logged_in(self.environment) {
return Err(ErrorData::invalid_request(
"Not logged in. Run `smb login` first.",
None,
));
}
get_smb_token(self.environment).map_err(|e| ErrorData::internal_error(e.to_string(), None))
}
}
fn json_result<T: serde::Serialize>(value: &T) -> Result<CallToolResult, ErrorData> {
Ok(CallToolResult::success(vec![ContentBlock::json(value)?]))
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ProjectShowArgs {
id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct DeploymentsArgs {
project_id: i32,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ProjectCreateArgs {
name: String,
#[serde(default)]
description: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ProjectUpdateArgs {
id: String,
description: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct ProjectDeleteArgs {
id: String,
}
#[tool_router]
impl SmbMcpServer {
#[tool(description = "Get the authenticated smbCloud user's account info. \
Requires a prior `smb login`; returns the user as JSON.")]
async fn me(&self) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
let user = me(self.environment, client(), &token)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
json_result(&user)
}
#[tool(description = "List the authenticated user's smbCloud projects as a JSON array.")]
async fn project_list(&self) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
let projects = get_projects(self.environment, client(), token)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
json_result(&projects)
}
#[tool(description = "Show a single smbCloud project by ID, returned as JSON.")]
async fn project_show(
&self,
Parameters(args): Parameters<ProjectShowArgs>,
) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
let project = get_project(self.environment, client(), token, args.id)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
json_result(&project)
}
#[tool(description = "List deployments for a project by project ID, returned as a JSON array.")]
async fn deployments(
&self,
Parameters(args): Parameters<DeploymentsArgs>,
) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
let deployments = get_deployments(self.environment, client(), token, args.project_id)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
json_result(&deployments)
}
#[tool(
description = "Create a new smbCloud project with a name and optional description. \
Returns the created project as JSON."
)]
async fn project_create(
&self,
Parameters(args): Parameters<ProjectCreateArgs>,
) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
let payload = ProjectCreate {
name: args.name,
description: args.description,
};
let project = create_project(self.environment, client(), token, payload)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
json_result(&project)
}
#[tool(
description = "Update a project's description by ID, preserving its runner. \
Returns the updated project as JSON."
)]
async fn project_update(
&self,
Parameters(args): Parameters<ProjectUpdateArgs>,
) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
let current = get_project(self.environment, client(), token.clone(), args.id.clone())
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
let project = update_project(
self.environment,
client(),
token,
args.id,
&args.description,
current.runner,
)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
json_result(&project)
}
#[tool(
description = "Delete a project by ID. This is destructive and irreversible — the \
project and its deploy configuration are removed."
)]
async fn project_delete(
&self,
Parameters(args): Parameters<ProjectDeleteArgs>,
) -> Result<CallToolResult, ErrorData> {
let token = self.access_token()?;
delete_project(self.environment, client(), token, args.id)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![ContentBlock::text(
"Project deleted.",
)]))
}
}
#[tool_handler]
impl ServerHandler for SmbMcpServer {
fn get_info(&self) -> ServerInfo {
let mut server_info = Implementation::from_build_env();
server_info.name = "smb".to_string();
server_info.version = env!("CARGO_PKG_VERSION").to_string();
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(server_info)
.with_instructions(
"smbCloud CLI exposed as MCP tools. Authentication uses the token stored by \
`smb login`; tools run non-interactively.",
)
}
}
pub async fn serve(environment: Environment) -> Result<()> {
let running = SmbMcpServer::new(environment)
.serve(stdio())
.await
.map_err(|e| anyhow!("Failed to start MCP server: {e}"))?;
running
.waiting()
.await
.map_err(|e| anyhow!("MCP server stopped unexpectedly: {e}"))?;
Ok(())
}