Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
#![expect(deprecated)]
use std::sync::Arc;
use std::{borrow::Cow, sync::Arc};

use crate::{
error::ErrorData as McpError,
Expand Down Expand Up @@ -30,11 +30,46 @@ impl<H: ServerHandler> Service<RoleServer> for H {
let mrtr_supported = protocol_version
.as_ref()
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
let requested_version = context.meta.protocol_version();
let uses_inline_negotiation = !matches!(&request, ClientRequest::InitializeRequest(_));
if uses_inline_negotiation && let Some(requested_version) = requested_version.as_ref() {
let supported_versions = self.supported_protocol_versions();
if !supported_versions.contains(requested_version) {
return Err(McpError::unsupported_protocol_version(
requested_version.clone(),
&supported_versions,
));
}
}
if matches!(&request, ClientRequest::DiscoverRequest(_)) {
if requested_version.is_none() {
return Err(McpError::invalid_params(
"server/discover requires protocolVersion in request _meta",
None,
));
}
if context.meta.client_info().is_none() {
return Err(McpError::invalid_params(
"server/discover requires clientInfo in request _meta",
None,
));
}
if context.meta.client_capabilities().is_none() {
return Err(McpError::invalid_params(
"server/discover requires clientCapabilities in request _meta",
None,
));
}
}
let result = match request {
ClientRequest::InitializeRequest(request) => self
.initialize(request.params, context)
.await
.map(ServerResult::InitializeResult),
ClientRequest::DiscoverRequest(_request) => self
.discover(context)
.await
.map(ServerResult::DiscoverResult),
ClientRequest::PingRequest(_request) => {
self.ping(context).await.map(ServerResult::empty)
}
Expand Down Expand Up @@ -225,6 +260,20 @@ macro_rules! server_handler_methods {
);
std::future::ready(Ok(info))
}
/// Return the protocol versions supported by this server.
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
Cow::Borrowed(ProtocolVersion::KNOWN_VERSIONS)
}
/// Return this server's discovery information.
fn discover(
&self,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Ok(DiscoverResult::from_server_info(
self.supported_protocol_versions().into_owned(),
self.get_info(),
)))
}
fn complete(
&self,
request: CompleteRequestParams,
Expand Down Expand Up @@ -479,6 +528,17 @@ macro_rules! impl_server_handler_for_wrapper {
(**self).initialize(request, context)
}

fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
(**self).supported_protocol_versions()
}

fn discover(
&self,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
(**self).discover(context)
}

fn complete(
&self,
request: CompleteRequestParams,
Expand Down
2 changes: 1 addition & 1 deletion crates/rmcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub use handler::server::wrapper::Json;
#[cfg(any(feature = "client", feature = "server"))]
pub use service::{Peer, Service, ServiceError, ServiceExt};
#[cfg(feature = "client")]
pub use service::{RoleClient, serve_client};
pub use service::{RoleClient, select_protocol_version, serve_client};
#[cfg(feature = "server")]
pub use service::{RoleServer, serve_server};

Expand Down
151 changes: 151 additions & 0 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,10 @@ pub struct JsonRpcNotification<N = Notification> {
pub struct ErrorCode(pub i32);

impl ErrorCode {
/// The request used a protocol version the server does not support.
pub const UNSUPPORTED_PROTOCOL_VERSION: Self = Self(-32022);
/// Processing the request requires a client capability that was not declared.
pub const MISSING_REQUIRED_CLIENT_CAPABILITY: Self = Self(-32021);
pub const HEADER_MISMATCH: Self = Self(-32020);
pub const RESOURCE_NOT_FOUND: Self = Self(-32002);
pub const INVALID_REQUEST: Self = Self(-32600);
Expand Down Expand Up @@ -568,6 +572,30 @@ impl ErrorData {
pub fn header_mismatch(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::HEADER_MISMATCH, message, data)
}
/// Create an unsupported-protocol-version error.
pub fn unsupported_protocol_version(
requested: ProtocolVersion,
supported: &[ProtocolVersion],
) -> Self {
Self::new(
ErrorCode::UNSUPPORTED_PROTOCOL_VERSION,
"Unsupported protocol version",
Some(serde_json::json!({
"requested": requested,
"supported": supported,
})),
)
}
/// Create a missing-required-capability error.
pub fn missing_required_client_capability(required: ClientCapabilities) -> Self {
Self::new(
ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY,
"Missing required client capability",
Some(serde_json::json!({
"requiredCapabilities": required,
})),
)
}
pub fn parse_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::PARSE_ERROR, message, data)
}
Expand Down Expand Up @@ -993,6 +1021,126 @@ impl InitializeResult {
pub type ServerInfo = InitializeResult;
pub type ClientInfo = InitializeRequestParams;

const_string!(DiscoverRequestMethod = "server/discover");

/// Parameters for [`DiscoverRequest`].
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct DiscoverRequestParams {}

#[cfg(feature = "schemars")]
#[derive(schemars::JsonSchema)]
#[expect(dead_code, reason = "schema-only representation of request metadata")]
struct DiscoverRequestMetaSchema {
#[schemars(rename = "io.modelcontextprotocol/protocolVersion")]
protocol_version: ProtocolVersion,
#[schemars(rename = "io.modelcontextprotocol/clientInfo")]
client_info: Implementation,
#[schemars(rename = "io.modelcontextprotocol/clientCapabilities")]
client_capabilities: ClientCapabilities,
#[schemars(rename = "io.modelcontextprotocol/logLevel")]
log_level: Option<LoggingLevel>,
}

#[cfg(feature = "schemars")]
#[derive(schemars::JsonSchema)]
#[expect(dead_code, reason = "schema-only representation of request parameters")]
struct DiscoverRequestParamsSchema {
#[schemars(rename = "_meta")]
meta: DiscoverRequestMetaSchema,
}

#[cfg(feature = "schemars")]
impl schemars::JsonSchema for DiscoverRequestParams {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("DiscoverRequestParams")
}

fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
DiscoverRequestParamsSchema::json_schema(generator)
}
}

/// A request for the server's supported protocol versions and capabilities.
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;

/// The server's response to a [`DiscoverRequest`].
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DiscoverResult {
/// Identifies how the result should be parsed.
pub result_type: ResultType,
/// Protocol versions implemented by this server.
pub supported_versions: Vec<ProtocolVersion>,
/// Capabilities provided by this server.
pub capabilities: ServerCapabilities,
/// Information about the server implementation.
pub server_info: Implementation,
/// Optional guidance for using the server.
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
/// How long clients may consider this response fresh, in milliseconds.
pub ttl_ms: u64,
/// Whether the cached result may be shared across authorization contexts.
pub cache_scope: CacheScope,
/// Protocol-level response metadata.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
}

impl DiscoverResult {
/// Create a non-cacheable private discovery result.
pub fn new(
supported_versions: Vec<ProtocolVersion>,
capabilities: ServerCapabilities,
server_info: Implementation,
) -> Self {
Self {
result_type: ResultType::COMPLETE,
supported_versions,
capabilities,
server_info,
instructions: None,
ttl_ms: 0,
cache_scope: CacheScope::Private,
meta: None,
}
}

/// Create a discovery result from the server's initialization information.
pub fn from_server_info(
supported_versions: Vec<ProtocolVersion>,
server_info: ServerInfo,
) -> Self {
let ServerInfo {
capabilities,
server_info,
instructions,
meta,
..
} = server_info;
let mut result = Self::new(supported_versions, capabilities, server_info);
result.instructions = instructions;
result.meta = meta;
result
}

/// Set the cache lifetime hint in milliseconds.
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
self.ttl_ms = ttl_ms;
self
}

/// Set the cache scope.
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
self.cache_scope = cache_scope;
self
}
}

#[allow(clippy::derivable_impls)]
impl Default for ServerInfo {
fn default() -> Self {
Expand Down Expand Up @@ -3788,6 +3936,7 @@ ts_union!(
export type ClientRequest =
| PingRequest
| InitializeRequest
| DiscoverRequest
| CompleteRequest
| SetLevelRequest
| GetPromptRequest
Expand All @@ -3811,6 +3960,7 @@ impl ClientRequest {
match &self {
ClientRequest::PingRequest(r) => r.method.as_str(),
ClientRequest::InitializeRequest(r) => r.method.as_str(),
ClientRequest::DiscoverRequest(r) => r.method.as_str(),
ClientRequest::CompleteRequest(r) => r.method.as_str(),
ClientRequest::SetLevelRequest(r) => r.method.as_str(),
ClientRequest::GetPromptRequest(r) => r.method.as_str(),
Expand Down Expand Up @@ -3882,6 +4032,7 @@ ts_union!(

ts_union!(
export type ServerResult =
| DiscoverResult
| InitializeResult
| CompleteResult
| GetPromptResult
Expand Down
1 change: 1 addition & 0 deletions crates/rmcp/src/model/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ variant_extension! {
ClientRequest {
PingRequest
InitializeRequest
DiscoverRequest
CompleteRequest
SetLevelRequest
GetPromptRequest
Expand Down
41 changes: 34 additions & 7 deletions crates/rmcp/src/service/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ use crate::{
ArgumentInfo, CallToolRequest, CallToolRequestParams, CallToolResponse, CallToolResult,
CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
ClientNotification, ClientRequest, ClientResult, CompleteRequest, CompleteRequestParams,
CompleteResult, CompletionContext, CompletionInfo, DEFAULT_MRTR_MAX_ROUNDS, ErrorData,
GetExtensions, GetMeta, GetPromptRequest, GetPromptRequestParams, GetPromptResponse,
GetPromptResult, InitializeRequest, InitializedNotification, InputRequest,
InputRequiredResult, InputResponses, JsonRpcResponse, ListPromptsRequest,
ListPromptsResult, ListResourceTemplatesRequest, ListResourceTemplatesResult,
ListResourcesRequest, ListResourcesResult, ListToolsRequest, ListToolsResult,
NumberOrString, PaginatedRequestParams, ProgressNotification, ProgressNotificationParam,
CompleteResult, CompletionContext, CompletionInfo, DEFAULT_MRTR_MAX_ROUNDS,
DiscoverRequest, DiscoverRequestParams, DiscoverResult, ErrorData, GetExtensions, GetMeta,
GetPromptRequest, GetPromptRequestParams, GetPromptResponse, GetPromptResult,
InitializeRequest, InitializedNotification, InputRequest, InputRequiredResult,
InputResponses, JsonRpcResponse, ListPromptsRequest, ListPromptsResult,
ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
ListResourcesResult, ListToolsRequest, ListToolsResult, Meta, NumberOrString,
PaginatedRequestParams, ProgressNotification, ProgressNotificationParam, ProtocolVersion,
ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult,
Reference, RequestId, RootsListChangedNotification, ServerInfo, ServerJsonRpcMessage,
ServerNotification, ServerRequest, ServerResult, SetLevelRequest, SetLevelRequestParams,
Expand Down Expand Up @@ -147,6 +148,19 @@ where
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct RoleClient;

/// Select the first client-preferred protocol version supported by the server.
///
/// Returns `None` when no version is shared.
pub fn select_protocol_version(
client_preference: &[ProtocolVersion],
server_supported: &[ProtocolVersion],
) -> Option<ProtocolVersion> {
client_preference
.iter()
.find(|version| server_supported.contains(version))
.cloned()
}

impl ServiceRole for RoleClient {
type Req = ClientRequest;
type Resp = ClientResult;
Expand Down Expand Up @@ -363,6 +377,19 @@ macro_rules! method {
}

impl Peer<RoleClient> {
/// Discover the server's supported protocol versions and capabilities.
pub async fn discover(&self, meta: Meta) -> Result<DiscoverResult, ServiceError> {
let mut request = DiscoverRequest::new(DiscoverRequestParams {});
request.extensions.insert(meta);
let result = self
.send_request(ClientRequest::DiscoverRequest(request))
.await?;
match result {
ServerResult::DiscoverResult(result) => Ok(result),
_ => Err(ServiceError::UnexpectedResponse),
}
}

/// Send one `tools/call` request and return either a final result or an MRTR
/// `InputRequiredResult` without driving any follow-up rounds.
pub async fn call_tool_once(
Expand Down
Loading