#![allow(clippy::unwrap_used)]
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use std::{borrow::Cow, net::SocketAddr, sync::Arc, time::Duration};
use polyc_controller::health_check;
use rmcp::{
ErrorData as McpError, ServerHandler,
handler::server::{
router::tool::ToolRouter,
tool::{ToolCallContext, ToolRoute},
},
model::{
CallToolRequestParams, CallToolResponse, CallToolResult, Implementation, InitializeResult,
ListToolsResult, PaginatedRequestParams, ServerCapabilities, Tool,
},
service::{RequestContext, RoleServer},
transport::streamable_http_server::{
StreamableHttpServerConfig, StreamableHttpService, session::never::NeverSessionManager,
},
};
use serde_json::json;
use tokio_util::sync::CancellationToken;
#[derive(Clone)]
struct CatalogServer {
router: Arc<ToolRouter<Self>>,
}
impl CatalogServer {
fn new() -> Self {
let mut router: ToolRouter<Self> = ToolRouter::new();
let echo_schema = json!({
"type": "object",
"properties": { "text": { "type": "string" } },
"required": ["text"],
});
let mut echo = Tool::new(
Cow::Borrowed("echo"),
Cow::Borrowed("Echo the input text back."),
echo_schema.as_object().cloned().unwrap_or_default(),
);
echo.annotations = Some(
echo.annotations
.unwrap_or_default()
.read_only(true)
.open_world(false),
);
router.add_route(ToolRoute::new_dyn(echo, |_ctx: ToolCallContext<Self>| {
Box::pin(async move { Ok(CallToolResult::structured(json!({ "ok": true })).into()) })
}));
let del_schema = json!({
"type": "object",
"properties": { "path": { "type": "string" } },
"required": ["path"],
});
let mut del = Tool::new(
Cow::Borrowed("delete_file"),
Cow::Borrowed("Delete a file."),
del_schema.as_object().cloned().unwrap_or_default(),
);
del.title = Some("Delete a file".to_owned());
del.annotations = Some(
del.annotations
.unwrap_or_default()
.read_only(false)
.destructive(true)
.open_world(true),
);
router.add_route(ToolRoute::new_dyn(del, |_ctx: ToolCallContext<Self>| {
Box::pin(async move { Ok(CallToolResult::structured(json!({ "ok": true })).into()) })
}));
Self {
router: Arc::new(router),
}
}
}
impl std::fmt::Debug for CatalogServer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CatalogServer").finish_non_exhaustive()
}
}
impl ServerHandler for CatalogServer {
fn get_info(&self) -> rmcp::model::ServerInfo {
InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new(
"catalog-fixture",
env!("CARGO_PKG_VERSION"),
))
}
fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListToolsResult, McpError>> + Send + '_ {
let tools = self.router.list_all();
async move { Ok(ListToolsResult::with_all_items(tools)) }
}
fn call_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResponse, McpError>> + Send + '_ {
let router = self.router.clone();
async move {
let ctx = ToolCallContext::new(self, request, context);
router.call(ctx).await
}
}
}
async fn spawn_server() -> (SocketAddr, CancellationToken, tokio::task::JoinHandle<()>) {
let mut config = StreamableHttpServerConfig::default();
config.legacy_session_mode = false;
config.sse_keep_alive = None;
let config = config.disable_allowed_hosts().disable_allowed_origins();
let service = StreamableHttpService::new(
|| Ok(CatalogServer::new()),
Arc::new(NeverSessionManager::default()),
config,
);
let router = axum::Router::new().nest_service("/mcp", service);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let ct = CancellationToken::new();
let server_ct = ct.clone();
let handle = tokio::spawn(async move {
let _ = axum::serve(listener, router)
.with_graceful_shutdown(async move { server_ct.cancelled_owned().await })
.await;
});
tokio::time::sleep(Duration::from_millis(50)).await;
(addr, ct, handle)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn health_check_reflects_full_specs_and_annotations() {
let (addr, ct, handle) = spawn_server().await;
let uri = format!("http://{addr}/mcp");
let readiness = health_check(&uri, None).await;
assert!(readiness.healthy, "fixture connector should list tools");
assert_eq!(readiness.available_tools.len(), 2);
let echo = readiness
.available_tools
.iter()
.find(|d| d.name == "echo")
.expect("echo reflected");
assert!(echo.read_only, "readOnlyHint rides the catalog");
assert!(!echo.destructive);
assert!(
!echo.open_world,
"explicit openWorldHint:false rides the catalog"
);
let echo_schema: serde_json::Value = serde_json::from_str(&echo.input_schema).unwrap();
assert_eq!(echo_schema["properties"]["text"]["type"], "string");
let del = readiness
.available_tools
.iter()
.find(|d| d.name == "delete_file")
.expect("delete_file reflected");
assert!(del.destructive, "destructiveHint rides the catalog");
assert!(del.open_world, "openWorldHint rides the catalog");
assert_eq!(del.title.as_deref(), Some("Delete a file"));
let del_schema: serde_json::Value = serde_json::from_str(&del.input_schema).unwrap();
assert_eq!(del_schema["required"][0], "path");
ct.cancel();
let _ = tokio::time::timeout(Duration::from_secs(5), handle).await;
}