pub mod auth;
pub mod error;
pub mod expose;
pub mod filter;
pub mod handlers;
pub mod schema;
pub mod server;
pub mod tools;
pub async fn run_server(
config: &crate::config::Config,
deployment_name: &str,
bind_addr: &str,
) -> anyhow::Result<()> {
use std::sync::Arc;
use crate::config::DeploymentRole;
let sliced = crate::config::slice::for_deployment(config, deployment_name)?;
let dep = sliced.deployments.first().ok_or_else(|| {
anyhow::anyhow!("no deployment found after slicing for '{deployment_name}'")
})?;
if dep.role != DeploymentRole::Mcp {
anyhow::bail!(
"quelch mcp requires a deployment with role=mcp, got '{:?}' for '{deployment_name}'",
dep.role
);
}
let cosmos: Arc<dyn crate::cosmos::CosmosBackend> = build_cosmos(&sliced).await?;
let search_service = sliced
.search
.service
.as_deref()
.unwrap_or("quelch-prod-search");
let search_endpoint = format!("https://{search_service}.search.windows.net");
let api_version = "2025-11-01-preview".to_string();
let search: Arc<dyn tools::search_api::SearchApiAdapter> = Arc::new(
tools::search_api::AzureSearchAdapter::new(search_endpoint, api_version)
.map_err(|e| anyhow::anyhow!("search adapter: {e}"))?,
);
let expose = Arc::new(
expose::ExposeResolver::from_sliced(&sliced, deployment_name)
.map_err(|e| anyhow::anyhow!("expose resolver: {e}"))?,
);
let schema = Arc::new(schema::SchemaCatalog::default());
let search_config = Arc::new(tools::search::SearchToolConfig {
disable_agentic: sliced
.mcp
.search
.as_ref()
.map(|s| s.disable_agentic)
.unwrap_or(false),
knowledge_base_name: sliced
.mcp
.search
.as_ref()
.and_then(|s| s.knowledge_base.clone())
.unwrap_or_else(|| "quelch-prod-kb".into()),
default_top: sliced.mcp.default_top as usize,
max_top: sliced.mcp.max_top as usize,
});
let state = server::ServerState {
cosmos,
search,
expose,
schema,
search_config,
};
server::serve(state, bind_addr).await
}
pub async fn run_server_in_memory(
config: &crate::config::Config,
deployment_name: &str,
bind_addr: &str,
cosmos: std::sync::Arc<dyn crate::cosmos::CosmosBackend>,
) -> anyhow::Result<()> {
use std::sync::Arc;
use crate::config::DeploymentRole;
let sliced = crate::config::slice::for_deployment(config, deployment_name)?;
let dep = sliced.deployments.first().ok_or_else(|| {
anyhow::anyhow!("no deployment found after slicing for '{deployment_name}'")
})?;
if dep.role != DeploymentRole::Mcp {
anyhow::bail!(
"run_server_in_memory requires a deployment with role=mcp, got '{:?}' for '{deployment_name}'",
dep.role
);
}
let search: Arc<dyn tools::search_api::SearchApiAdapter> =
Arc::new(tools::search_api::NoOpSearch);
let expose = Arc::new(
expose::ExposeResolver::from_sliced(&sliced, deployment_name)
.map_err(|e| anyhow::anyhow!("expose resolver: {e}"))?,
);
let schema = Arc::new(schema::SchemaCatalog::default());
let search_config = Arc::new(tools::search::SearchToolConfig {
disable_agentic: true, knowledge_base_name: "dev-kb".into(),
default_top: sliced.mcp.default_top as usize,
max_top: sliced.mcp.max_top as usize,
});
let state = server::ServerState {
cosmos,
search,
expose,
schema,
search_config,
};
server::serve(state, bind_addr).await
}
async fn build_cosmos(
config: &crate::config::Config,
) -> anyhow::Result<std::sync::Arc<dyn crate::cosmos::CosmosBackend>> {
use crate::config::StateBackend;
match &config.state.backend {
StateBackend::Cosmos => {
let account = config.cosmos.account.as_deref().ok_or_else(|| {
anyhow::anyhow!("cosmos.account is required when state.backend=cosmos")
})?;
let endpoint = if account.starts_with("https://") {
account.to_owned()
} else {
format!("https://{account}.documents.azure.com:443/")
};
let client =
crate::cosmos::CosmosClient::new(&endpoint, &config.cosmos.database).await?;
Ok(std::sync::Arc::new(client))
}
StateBackend::LocalFile => {
anyhow::bail!(
"state.backend=local_file is not supported for the MCP server; use cosmos"
)
}
}
}