use std::sync::Arc;
use clap::Parser;
use rmcp::transport::streamable_http_server::{
StreamableHttpService, session::local::LocalSessionManager,
};
use axum::routing::get_service;
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::fs::ServeDir;
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
use utoipa_swagger_ui::SwaggerUi;
mod chat;
pub mod config;
pub mod db;
pub mod error;
pub mod index;
pub mod mcp;
pub mod sparql_client;
mod utils;
mod validate;
pub mod void_schema;
use crate::db::SearchDB;
use crate::error::AppResult;
use crate::mcp::SparqlTools;
use crate::void_schema::SchemasMap;
/// Command line arguments for the `sparql-mcp` server
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
/// Optional path to a JSON file containing SPARQL endpoints config
#[arg(value_name = "CONFIG_JSON", required = false)]
pub config_json: Option<String>,
/// Use STDIO transport instead of HTTP
#[arg(short = 's', long = "stdio", env = "STDIO", default_value_t = false)]
pub stdio: bool,
/// Address where to deploy this server
#[arg(
short = 'b',
long = "bind",
env = "BIND_ADDRESS",
default_value = "0.0.0.0:8000"
)]
pub bind_address: String,
/// Path where LanceDB will store the search index
#[arg(
short = 'd',
long = "db-path",
env = "DB_PATH",
default_value = "./data/lancedb"
)]
pub db_path: String,
/// Only deploy the MCP endpoint without the search endpoint
#[arg(short = 'm', long = "mcp-only", default_value_t = false)]
pub mcp_only: bool,
/// Force reindexing of the SPARQL endpoints at startup
#[arg(short = 'f', long = "force-index", default_value_t = false)]
pub force_index: bool,
/// Automatically index any unknown endpoint when asked about it
#[arg(
short = 'a',
long = "auto-index",
env = "AUTO_INDEX",
default_value_t = false
)]
pub auto_index: bool,
}
/// OpenAPI documentation for the API
#[derive(OpenApi)]
#[openapi(info(
title = "SPARQL MCP server",
version = "1.0.0",
description = r#"MCP server to help generating SPARQL queries for questions in natural language, developed for the [SIB Expasy project](https://www.expasy.org).
If asked about an endpoint it does not know it will automatically try to index it.
[Source code](https://github.com/sib-swiss/sparql-mcp)"#
))]
struct ApiDoc;
#[derive(Clone)]
pub struct AppState {
pub bind_address: String,
pub schemas_map: SchemasMap,
pub db: Arc<SearchDB>,
}
/// Build the Axum router with the configured endpoints and services
pub async fn build_router(
args: &Args,
db: &Arc<SearchDB>,
schemas_map: &SchemasMap,
) -> AppResult<axum::Router> {
// // Load endpoints config
// let endpoints_config = if let Some(ref json_path) = args.config_json {
// EndpointsConfig::from_json_file(json_path)?
// } else {
// EndpointsConfig::default()
// };
// // Init search DB
// let (db, schemas_map) =
// index::init_db(&endpoints_config, &args.db_path, args.force_index).await?;
// Init web app state
let app_state = AppState {
bind_address: args.bind_address.clone(),
schemas_map: schemas_map.clone(),
db: db.clone(),
};
let cors = CorsLayer::new().allow_origin(Any).allow_headers([
axum::http::header::AUTHORIZATION,
axum::http::header::CONTENT_TYPE,
]);
// Init MCP service
let auto_index = args.auto_index;
let db = db.clone();
let schemas_map = schemas_map.clone();
let mcp_service = StreamableHttpService::new(
move || {
Ok(SparqlTools::new(
db.clone(),
schemas_map.clone(),
auto_index,
))
},
LocalSessionManager::default().into(),
Default::default(),
);
// Build router
let router = if args.mcp_only {
axum::Router::new()
.nest_service("/mcp", mcp_service)
.layer(cors)
} else {
let webapp_service =
get_service(ServeDir::new("src/webapp")).handle_error(|error| async move {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal error: {error}"),
)
});
let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
.routes(routes!(chat::chat_handler))
// TODO: add route to get list of endpoints currently supported
// .routes(routes!(chat::list_endpoints))
.with_state(app_state)
.split_for_parts();
router
.fallback_service(webapp_service)
.nest_service("/mcp", mcp_service)
.merge(SwaggerUi::new("/docs").url("/openapi.json", api))
.layer(cors)
};
Ok(router)
}