use std::borrow::Cow;
use std::collections::HashMap;
use std::future::ready;
use std::sync::{Arc, Mutex, PoisonError};
use http::request::Parts;
use http::{HeaderName, HeaderValue, Request};
use rmcp::handler::server::common::Extension;
use rmcp::handler::server::wrapper::{Json, Parameters};
use rmcp::model::{
CallToolRequestParams, CallToolResponse, CallToolResult, CancelTaskParams, ClientCapabilities,
CompleteRequestParams, CompleteResult, CompletionInfo, ContentBlock, CustomRequest,
CustomResult, DiscoverResult, GetPromptRequestParams, GetPromptResponse, GetPromptResult,
GetTaskParams, GetTaskResult, Implementation, InitializeRequestParams, InitializeResult,
InputRequiredResult, JsonObject, ListPromptsResult, ListResourceTemplatesResult,
ListResourcesResult, ListToolsResult, MetaObject, PaginatedRequestParams, Prompt,
ProtocolVersion, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult,
RequestId, Resource, ResourceContents, ResourceTemplate, ServerCapabilities,
SubscribeRequestParams, SubscriptionFilter, Tool, UnsubscribeRequestParams, UpdateTaskParams,
};
use rmcp::service::{
MaybeSendFuture, RequestContext, RoleClient, RunningService, SubscriptionContext,
serve_directly,
};
use rmcp::transport::StreamableHttpClientTransport;
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use rmcp::transport::streamable_http_server::{StreamableHttpServerConfig, StreamableHttpService};
use rmcp::{ErrorData, RoleServer, ServerHandler, ServiceExt, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use crate::context::CallContext;
use crate::event::assemble::EventJob;
use crate::event::{CallRecord, ClientIdentity, client_identity, extra_value, request_value};
use crate::sink::Sink;
const PIPE_BYTES: usize = 64 * 1024;
pub(crate) async fn connect<S: ServerHandler>(
server: S,
client_name: &str,
) -> RunningService<RoleClient, InitializeRequestParams> {
let (server_end, client_end) = tokio::io::duplex(PIPE_BYTES);
tokio::spawn(async move {
let running = server.serve(server_end).await.expect("the server starts");
running.waiting().await.expect("the server stops cleanly");
});
InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new(client_name, "1.0.0"),
)
.with_protocol_version(ProtocolVersion::V_2025_11_25)
.serve(client_end)
.await
.expect("the client connects")
}
pub(crate) const HTTP_PATH: &str = "/mcp";
pub(crate) async fn serve_http<S: ServerHandler + Clone>(
server: S,
config: StreamableHttpServerConfig,
) -> String {
let service: StreamableHttpService<S, LocalSessionManager> = StreamableHttpService::new(
move || Ok(server.clone()),
Arc::new(LocalSessionManager::default()),
config,
);
let router = axum::Router::new().route_service(HTTP_PATH, service);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("a loopback port is free");
let address = listener.local_addr().expect("the listener has an address");
tokio::spawn(async move {
axum::serve(listener, router)
.await
.expect("the HTTP server runs");
});
format!("http://{address}{HTTP_PATH}")
}
pub(crate) async fn connect_http(
url: &str,
client_name: &str,
headers: HashMap<HeaderName, HeaderValue>,
) -> RunningService<RoleClient, InitializeRequestParams> {
let config =
StreamableHttpClientTransportConfig::with_uri(url.to_owned()).custom_headers(headers);
InitializeRequestParams::new(
ClientCapabilities::default(),
Implementation::new(client_name, "1.0.0"),
)
.with_protocol_version(ProtocolVersion::V_2025_11_25)
.serve(StreamableHttpClientTransport::from_config(config))
.await
.expect("the HTTP client connects")
}
pub(crate) fn bare_context(
peer_info: Option<InitializeRequestParams>,
) -> RequestContext<RoleServer> {
let (server_end, _client_end) = tokio::io::duplex(PIPE_BYTES);
let running = serve_directly::<RoleServer, _, _, _, _>(Probe, server_end, peer_info);
RequestContext::new(RequestId::Number(1), running.peer().clone())
}
pub(crate) const CREDENTIAL_HEADERS: [(&str, &str); 4] = [
("authorization", "Bearer credential-authorization-1"),
("proxy-authorization", "Basic credential-proxy-2"),
("cookie", "sid=credential-cookie-3"),
("set-cookie", "sid=credential-set-cookie-4"),
];
pub(crate) const ORDINARY_HEADER: (&str, &str) = ("x-test-header", "ordinary-value-5");
pub(crate) fn credential_parts() -> Parts {
let mut builder = Request::builder()
.method("POST")
.uri(HTTP_PATH)
.header("Host", "localhost:8080");
for (name, value) in CREDENTIAL_HEADERS {
builder = builder.header(name.to_uppercase(), value);
}
let (name, value) = ORDINARY_HEADER;
let request = builder.header(name, value).body(()).expect("valid parts");
request.into_parts().0
}
pub(crate) fn assert_credentials_hidden(text: &str) {
for (name, value) in CREDENTIAL_HEADERS {
assert!(
text.contains(&format!("{name:?}")),
"{name} is missing: {text}"
);
assert!(!text.contains(value), "the value of {name} leaks: {text}");
}
assert!(
text.contains(ORDINARY_HEADER.1),
"the ordinary value is missing: {text}"
);
}
pub(crate) const PROBE_VERSIONS: &[ProtocolVersion] =
&[ProtocolVersion::V_2025_11_25, ProtocolVersion::V_2026_07_28];
#[derive(Debug, Clone, Copy)]
pub(crate) struct Probe;
fn probe_error(method: &str) -> ErrorData {
ErrorData::internal_error(format!("probe {method}"), None)
}
impl ServerHandler for Probe {
fn ping(
&self,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Err(probe_error("ping")))
}
fn initialize(
&self,
request: InitializeRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<InitializeResult, ErrorData>> + MaybeSendFuture + '_ {
context.peer.set_peer_info(request);
let mut info = self.get_info();
info.server_info.title = Some("probe initialize".to_owned());
ready(Ok(info))
}
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
Cow::Borrowed(PROBE_VERSIONS)
}
fn discover(
&self,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<DiscoverResult, ErrorData>> + MaybeSendFuture + '_ {
let mut result = DiscoverResult::from_server_info(PROBE_VERSIONS.to_vec(), self.get_info());
result.instructions = Some("probe discover".to_owned());
ready(Ok(result))
}
fn complete(
&self,
_request: CompleteRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CompleteResult, ErrorData>> + MaybeSendFuture + '_ {
let values = vec!["probe complete".to_owned()];
ready(
CompletionInfo::with_all_values(values)
.map(CompleteResult::new)
.map_err(|e| ErrorData::internal_error(e, None)),
)
}
#[expect(
deprecated,
reason = "SetLevelRequestParams is SEP-2577-deprecated on every rmcp 3.x"
)]
fn set_level(
&self,
_request: rmcp::model::SetLevelRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Ok(()))
}
fn get_prompt(
&self,
_request: GetPromptRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetPromptResponse, ErrorData>> + MaybeSendFuture + '_ {
let result = GetPromptResult::new(Vec::new()).with_description("probe get_prompt");
ready(Ok(GetPromptResponse::Complete(result)))
}
fn list_prompts(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListPromptsResult, ErrorData>> + MaybeSendFuture + '_ {
let prompt = Prompt::new("greeting", Some("probe list_prompts"), None);
ready(Ok(ListPromptsResult::with_all_items(vec![prompt])))
}
fn list_resources(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListResourcesResult, ErrorData>> + MaybeSendFuture + '_ {
let resource = Resource::new("file:///greeting.txt", "greeting")
.with_description("probe list_resources");
ready(Ok(ListResourcesResult::with_all_items(vec![resource])))
}
fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListResourceTemplatesResult, ErrorData>> + MaybeSendFuture + '_
{
let template = ResourceTemplate::new("file:///{name}.txt", "greeting")
.with_description("probe list_resource_templates");
ready(Ok(ListResourceTemplatesResult::with_all_items(vec![
template,
])))
}
fn read_resource(
&self,
request: ReadResourceRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ReadResourceResponse, ErrorData>> + MaybeSendFuture + '_ {
let contents = ResourceContents::text("probe read_resource", request.uri);
let result = ReadResourceResult::new(vec![contents]);
ready(Ok(ReadResourceResponse::Complete(result)))
}
fn accepted_subscription_filter(
&self,
requested: &SubscriptionFilter,
) -> Option<SubscriptionFilter> {
Some(requested.clone())
}
fn listen(
&self,
_context: SubscriptionContext,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Err(probe_error("listen")))
}
fn subscribe(
&self,
_request: SubscribeRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Ok(()))
}
fn unsubscribe(
&self,
_request: UnsubscribeRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Ok(()))
}
fn call_tool(
&self,
_request: CallToolRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResponse, ErrorData>> + MaybeSendFuture + '_ {
ready(Err(probe_error("call_tool")))
}
fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListToolsResult, ErrorData>> + MaybeSendFuture + '_ {
let mut result = ListToolsResult::with_all_items(Vec::new());
let mut meta = MetaObject::default();
meta.0.insert("note".to_owned(), json!("probe list_tools"));
result.meta = Some(meta);
ready(Ok(result))
}
fn on_custom_request(
&self,
request: CustomRequest,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CustomResult, ErrorData>> + MaybeSendFuture + '_ {
let answer = json!({"method": request.method, "note": "probe on_custom_request"});
ready(Ok(CustomResult(answer)))
}
fn get_info(&self) -> InitializeResult {
let mut info = InitializeResult::new(ServerCapabilities::builder().enable_tasks().build());
info.protocol_version = ProtocolVersion::V_2025_11_25;
info.server_info = Implementation::new("probe", "1.0.0");
info.instructions = Some("probe".to_owned());
info
}
fn get_task(
&self,
_request: GetTaskParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetTaskResult, ErrorData>> + MaybeSendFuture + '_ {
ready(Err(probe_error("get_task")))
}
fn update_task(
&self,
_request: UpdateTaskParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Ok(()))
}
fn cancel_task(
&self,
_request: CancelTaskParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), ErrorData>> + MaybeSendFuture + '_ {
ready(Ok(()))
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Paged;
pub(crate) const PAGED_CURSOR: &str = "p2";
fn paged_properties(name: &str) -> Option<&'static [&'static str]> {
match name {
"alpha" => Some(&["x"]),
"beta" => Some(&["x", "context"]),
_ => None,
}
}
fn paged_schema(name: &str) -> JsonObject {
let properties: Map<String, Value> = paged_properties(name)
.unwrap_or_default()
.iter()
.map(|property| ((*property).to_owned(), json!({"type": "string"})))
.collect();
let schema = json!({"type": "object", "properties": properties});
schema.as_object().cloned().unwrap_or_default()
}
fn paged_call(request: CallToolRequestParams) -> Result<CallToolResponse, ErrorData> {
let Some(own) = paged_properties(&request.name) else {
return Err(ErrorData::invalid_params("tool not found", None));
};
let arguments = request.arguments.unwrap_or_default();
if let Some(unknown) = arguments.keys().find(|key| !own.contains(&key.as_str())) {
let message = format!("unknown field `{unknown}`");
return Err(ErrorData::invalid_params(message, None));
}
Ok(CallToolResult::structured(Value::Object(arguments)).into())
}
impl ServerHandler for Paged {
fn list_tools(
&self,
request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListToolsResult, ErrorData>> + MaybeSendFuture + '_ {
let second = request.and_then(|r| r.cursor).as_deref() == Some(PAGED_CURSOR);
let result = if second {
let beta = Tool::new("beta", "page 2", paged_schema("beta"));
ListToolsResult::with_all_items(vec![beta])
} else {
let alpha = Tool::new("alpha", "page 1", paged_schema("alpha"));
let mut result = ListToolsResult::with_all_items(vec![alpha]);
result.next_cursor = Some(PAGED_CURSOR.to_owned());
let mut meta = MetaObject::default();
meta.0.insert("note".to_owned(), json!("paged page 1"));
result.meta = Some(meta);
result
};
ready(Ok(result))
}
fn call_tool(
&self,
request: CallToolRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResponse, ErrorData>> + MaybeSendFuture + '_ {
ready(paged_call(request))
}
fn get_info(&self) -> InitializeResult {
InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SilentPaged;
impl ServerHandler for SilentPaged {
fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListToolsResult, ErrorData>> + MaybeSendFuture + '_ {
std::future::pending()
}
fn call_tool(
&self,
request: CallToolRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResponse, ErrorData>> + MaybeSendFuture + '_ {
ready(paged_call(request))
}
fn get_info(&self) -> InitializeResult {
Paged.get_info()
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct EchoArgs {
msg: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct StrictArgs {
text: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct OwnContextArgs {
context: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct OwnSessionArgs {
session_id: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub(crate) struct Structured {
msg: String,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct Sample;
#[tool_router]
impl Sample {
#[tool(description = "Echoes msg")]
async fn echo(&self, Parameters(args): Parameters<EchoArgs>) -> String {
args.msg
}
#[tool(description = "Echoes text; refuses unknown fields")]
async fn strict(&self, Parameters(args): Parameters<StrictArgs>) -> String {
args.text
}
#[tool(description = "Echoes msg as structured content")]
async fn structured(&self, Parameters(args): Parameters<EchoArgs>) -> Json<Structured> {
Json(Structured { msg: args.msg })
}
#[tool(description = "Declares its own context")]
async fn own_context(&self, Parameters(args): Parameters<OwnContextArgs>) -> String {
args.context
}
#[tool(description = "Declares its own session_id")]
async fn own_session(&self, Parameters(args): Parameters<OwnSessionArgs>) -> String {
args.session_id
}
#[tool(description = "Always returns an error")]
async fn fail(&self) -> Result<String, String> {
Err("failed".to_owned())
}
#[tool(description = "Panics")]
async fn explode(&self) -> String {
panic!("explode")
}
#[tool(description = "Answers the session and agent of the call")]
async fn whoami(&self, Extension(call): Extension<CallContext>) -> String {
json!({"session_id": call.session_id(), "agent_id": call.agent_id()}).to_string()
}
}
#[expect(
clippy::unused_async_trait_impl,
reason = "the methods #[tool_handler] generates are async without an await"
)]
#[tool_handler]
impl ServerHandler for Sample {}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ContextEcho;
impl ServerHandler for ContextEcho {
fn call_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResponse, ErrorData>> + MaybeSendFuture + '_ {
let peer = context
.peer
.peer_info()
.map(|info| ClientIdentity::from(&info.client_info));
let echo = json!({
"request": request_value(&request, &context),
"extra": extra_value(&context),
"client": identity_json(client_identity(&context).as_ref()),
"peer": identity_json(peer.as_ref()),
});
ready(Ok(CallToolResult::structured(echo).into()))
}
fn get_info(&self) -> InitializeResult {
InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
}
}
pub(crate) const MRTR_STATE: &str = "mrtr-state";
#[derive(Debug, Clone, Copy)]
pub(crate) struct Mrtr;
impl ServerHandler for Mrtr {
fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ListToolsResult, ErrorData>> + MaybeSendFuture + '_ {
let schema = json!({"type": "object"}).as_object().cloned();
let ask = Tool::new("ask", "Asks the client", schema.unwrap_or_default());
ready(Ok(ListToolsResult::with_all_items(vec![ask])))
}
fn call_tool(
&self,
request: CallToolRequestParams,
_context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResponse, ErrorData>> + MaybeSendFuture + '_ {
let response = if request.input_responses.is_some() {
CallToolResult::success(vec![ContentBlock::text("answered")]).into()
} else {
InputRequiredResult::from_request_state(MRTR_STATE).into()
};
ready(Ok(response))
}
fn get_info(&self) -> InitializeResult {
InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
}
}
pub(crate) fn identity_json(identity: Option<&ClientIdentity>) -> Value {
let Some(identity) = identity else {
return Value::Null;
};
let mut object = Map::new();
if let Some(name) = &identity.name {
object.insert("name".to_owned(), json!(name));
}
if let Some(version) = &identity.version {
object.insert("version".to_owned(), json!(version));
}
Value::Object(object)
}
#[derive(Default)]
pub(crate) struct RecordingSink {
logs: Mutex<Vec<String>>,
records: Mutex<Vec<CallRecord>>,
}
impl RecordingSink {
pub(crate) fn logs(&self) -> Vec<String> {
self.logs
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub(crate) fn take_records(&self) -> Vec<CallRecord> {
std::mem::take(&mut *self.records.lock().unwrap_or_else(PoisonError::into_inner))
}
}
impl Sink for RecordingSink {
fn log(&self, message: String) {
self.logs
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(message);
}
fn record(&self, record: CallRecord) {
self.records
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(record);
}
fn event(&self, _job: EventJob) {}
}