use rmcp::handler::server::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{ServerCapabilities, ServerInfo};
use rmcp::{ServerHandler, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Deserialize, JsonSchema)]
pub struct ProjectParams {
#[schemars(default)]
pub project: Option<String>,
#[schemars(default)]
pub env: Option<String>,
}
#[derive(Deserialize, JsonSchema)]
pub struct ValidateParams {
#[schemars(default)]
pub project: Option<String>,
#[schemars(default)]
pub strict: Option<bool>,
}
#[derive(Deserialize, JsonSchema)]
pub struct DiffParams {
#[schemars(default)]
pub project: Option<String>,
#[schemars(default)]
pub env: Option<String>,
#[schemars(default)]
pub only: Option<String>,
#[schemars(default)]
pub compare_env: Option<String>,
}
#[derive(Deserialize, JsonSchema)]
pub struct PullParams {
#[schemars(default)]
pub project: Option<String>,
#[schemars(default)]
pub env: Option<String>,
#[schemars(default)]
pub adopt: Option<bool>,
#[schemars(default)]
pub force: Option<bool>,
}
#[derive(Deserialize, JsonSchema)]
pub struct PushParams {
#[schemars(default)]
pub project: Option<String>,
#[schemars(default)]
pub env: Option<String>,
#[schemars(default)]
pub prune: Option<bool>,
#[schemars(default)]
pub force: Option<bool>,
}
#[derive(Deserialize, JsonSchema)]
pub struct DeleteParams {
pub project: String,
#[schemars(default)]
pub env: Option<String>,
#[schemars(default)]
pub force: Option<bool>,
}
#[derive(Clone)]
pub struct RiggMcpServer {
tool_router: ToolRouter<Self>,
}
impl RiggMcpServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
impl Default for RiggMcpServer {
fn default() -> Self {
Self::new()
}
}
fn rigg_cli(args: &[&str]) -> String {
let exe = match std::env::current_exe() {
Ok(exe) => exe,
Err(e) => return format!("Error: cannot locate rigg executable: {e}"),
};
let output = std::process::Command::new(exe)
.args(args)
.env("RIGG_NO_UPDATE_CHECK", "1")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
let code = out.status.code().unwrap_or(-1);
match code {
0 => stdout,
3 => format!("VALIDATION FAILED (exit 3)\n{stdout}\n{stderr}"),
4 => format!("AUTH/PERMISSION DENIED (exit 4)\n{stdout}\n{stderr}"),
5 => format!("DRIFT/CONFLICT DETECTED (exit 5)\n{stdout}\n{stderr}"),
_ => format!("ERROR (exit {code})\n{stdout}\n{stderr}"),
}
}
Err(e) => format!("Error: failed to run rigg: {e}"),
}
}
fn with_common<'a>(mut args: Vec<&'a str>, env: &'a Option<String>, json: bool) -> Vec<&'a str> {
if let Some(env) = env {
args.push("--env");
args.push(env);
}
if json {
args.push("--output");
args.push("json");
}
args.push("--quiet");
args
}
#[tool_router]
impl RiggMcpServer {
#[tool(
description = "Show sync status per project: which resources are in sync, local-ahead, remote-ahead, conflicted, plus unmanaged remote resources."
)]
async fn rigg_status(&self, Parameters(params): Parameters<ProjectParams>) -> String {
let mut args = vec!["status"];
if let Some(p) = ¶ms.project {
args.push(p);
}
rigg_cli(&with_common(args, ¶ms.env, true))
}
#[tool(
description = "Full workspace description: projects, all resources with definitions and file paths, the dependency graph, and 'APIs to implement' (OpenAPI specs in apis/ that skillsets reference). The fastest way to understand the workspace."
)]
async fn rigg_describe(&self, Parameters(params): Parameters<ProjectParams>) -> String {
let mut args = vec!["describe"];
if let Some(p) = ¶ms.project {
args.push(p);
}
rigg_cli(&with_common(args, ¶ms.env, true))
}
#[tool(description = "List all configured deployment environments from rigg.yaml")]
async fn rigg_env_list(&self) -> String {
rigg_cli(&["env", "list", "--output", "json", "--quiet"])
}
#[tool(
description = "Validate local files: JSON structure, name/filename consistency, exclusive ownership across projects, reference resolution, no-secrets enforcement, data source types. Exit 3 = problems found."
)]
async fn rigg_validate(&self, Parameters(params): Parameters<ValidateParams>) -> String {
let mut args = vec!["validate"];
if let Some(p) = ¶ms.project {
args.push(p);
}
if params.strict.unwrap_or(false) {
args.push("--strict");
}
args.extend(["--output", "json", "--quiet"]);
rigg_cli(&args)
}
#[tool(
description = "Semantic diff of local project files vs live Azure (or one env vs another with compare_env). Volatile server fields are ignored; array order does not matter."
)]
async fn rigg_diff(&self, Parameters(params): Parameters<DiffParams>) -> String {
let mut args = vec!["diff"];
if let Some(p) = ¶ms.project {
args.push(p);
}
args.extend(["--format", "json"]);
if let Some(only) = ¶ms.only {
args.extend(["--only", only]);
}
if let Some(ce) = ¶ms.compare_env {
args.extend(["--compare-env", ce]);
}
rigg_cli(&with_common(args, ¶ms.env, false))
}
#[tool(
description = "Pull remote resource definitions into the project's files. Without force: returns the diff (preview). With force=true: executes the pull (--yes). adopt=true additionally claims unmanaged remote resources into the project."
)]
async fn rigg_pull(&self, Parameters(params): Parameters<PullParams>) -> String {
if !params.force.unwrap_or(false) {
let mut args = vec!["diff"];
if let Some(p) = ¶ms.project {
args.push(p);
}
args.extend(["--format", "json"]);
let preview = rigg_cli(&with_common(args, ¶ms.env, false));
return format!(
"PREVIEW (no changes made) — differences between local and remote:\n{preview}\nRun again with force=true to pull."
);
}
let mut args = vec!["pull"];
if let Some(p) = ¶ms.project {
args.push(p);
}
if params.adopt.unwrap_or(false) {
let project = params.project.as_deref().unwrap_or_default();
if project.is_empty() {
return "Error: adopt=true requires an explicit project".to_string();
}
args.extend(["--adopt", project]);
}
args.push("--yes");
rigg_cli(&with_common(args, ¶ms.env, false))
}
#[tool(
description = "Push local project files to Azure in dependency order. Without force: returns the push plan (dry run). With force=true: executes (--yes). prune=true also deletes remote resources whose local files were removed. Always rigg_validate first."
)]
async fn rigg_push(&self, Parameters(params): Parameters<PushParams>) -> String {
let mut args = vec!["push"];
if let Some(p) = ¶ms.project {
args.push(p);
}
if params.prune.unwrap_or(false) {
args.push("--prune");
}
if params.force.unwrap_or(false) {
args.push("--yes");
} else {
args.push("--dry-run");
}
rigg_cli(&with_common(args, ¶ms.env, false))
}
#[tool(
description = "Delete ALL of a project's resources from Azure (local files are kept — pushing re-creates everything). Without force: preview. With force=true: executes. For deleting a single resource: delete its local file, then rigg_push with prune=true."
)]
async fn rigg_delete(&self, Parameters(params): Parameters<DeleteParams>) -> String {
if !params.force.unwrap_or(false) {
let status = self
.rigg_status(Parameters(ProjectParams {
project: Some(params.project.clone()),
env: params.env.clone(),
}))
.await;
return format!(
"PREVIEW (no changes made) — deleting project '{}' would remove its remote resources. Current state:\n{status}\nRun again with force=true to delete.",
params.project
);
}
let mut args = vec!["delete", params.project.as_str(), "--remote", "--yes"];
if let Some(env) = ¶ms.env {
args.extend(["--env", env]);
}
args.push("--quiet");
rigg_cli(&args)
}
}
#[tool_handler]
impl ServerHandler for RiggMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
instructions: Some(
"rigg manages Azure AI Search and Microsoft Foundry configuration as code. \
A workspace contains projects; each project owns its resources exclusively, \
and pull/push/diff operate on whole projects. Typical flow: rigg_describe to \
understand the workspace, rigg_validate before changes, rigg_diff to inspect \
drift, rigg_push (preview first, then force=true). Resource definitions are \
JSON files under projects/<name>/{search,foundry}/<kind>/; secrets are never \
stored in files — identity-based access only."
.to_string(),
),
..Default::default()
}
}
}