Skip to content

Commit 4d23aa9

Browse files
committed
feat!: add server discovery and negotiation
1 parent 839922d commit 4d23aa9

13 files changed

Lines changed: 1240 additions & 69 deletions

crates/rmcp/src/handler/server.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
22
#![expect(deprecated)]
3-
use std::sync::Arc;
3+
use std::{borrow::Cow, sync::Arc};
44

55
use crate::{
66
error::ErrorData as McpError,
@@ -30,11 +30,46 @@ impl<H: ServerHandler> Service<RoleServer> for H {
3030
let mrtr_supported = protocol_version
3131
.as_ref()
3232
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
33+
let requested_version = context.meta.protocol_version();
34+
let uses_inline_negotiation = !matches!(&request, ClientRequest::InitializeRequest(_));
35+
if uses_inline_negotiation && let Some(requested_version) = requested_version.as_ref() {
36+
let supported_versions = self.supported_protocol_versions();
37+
if !supported_versions.contains(requested_version) {
38+
return Err(McpError::unsupported_protocol_version(
39+
requested_version.clone(),
40+
&supported_versions,
41+
));
42+
}
43+
}
44+
if matches!(&request, ClientRequest::DiscoverRequest(_)) {
45+
if requested_version.is_none() {
46+
return Err(McpError::invalid_params(
47+
"server/discover requires protocolVersion in request _meta",
48+
None,
49+
));
50+
}
51+
if context.meta.client_info().is_none() {
52+
return Err(McpError::invalid_params(
53+
"server/discover requires clientInfo in request _meta",
54+
None,
55+
));
56+
}
57+
if context.meta.client_capabilities().is_none() {
58+
return Err(McpError::invalid_params(
59+
"server/discover requires clientCapabilities in request _meta",
60+
None,
61+
));
62+
}
63+
}
3364
let result = match request {
3465
ClientRequest::InitializeRequest(request) => self
3566
.initialize(request.params, context)
3667
.await
3768
.map(ServerResult::InitializeResult),
69+
ClientRequest::DiscoverRequest(_request) => self
70+
.discover(context)
71+
.await
72+
.map(ServerResult::DiscoverResult),
3873
ClientRequest::PingRequest(_request) => {
3974
self.ping(context).await.map(ServerResult::empty)
4075
}
@@ -225,6 +260,20 @@ macro_rules! server_handler_methods {
225260
);
226261
std::future::ready(Ok(info))
227262
}
263+
/// Return the protocol versions supported by this server.
264+
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
265+
Cow::Borrowed(ProtocolVersion::KNOWN_VERSIONS)
266+
}
267+
/// Return this server's discovery information.
268+
fn discover(
269+
&self,
270+
context: RequestContext<RoleServer>,
271+
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
272+
std::future::ready(Ok(DiscoverResult::from_server_info(
273+
self.supported_protocol_versions().into_owned(),
274+
self.get_info(),
275+
)))
276+
}
228277
fn complete(
229278
&self,
230279
request: CompleteRequestParams,
@@ -479,6 +528,17 @@ macro_rules! impl_server_handler_for_wrapper {
479528
(**self).initialize(request, context)
480529
}
481530

531+
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
532+
(**self).supported_protocol_versions()
533+
}
534+
535+
fn discover(
536+
&self,
537+
context: RequestContext<RoleServer>,
538+
) -> impl Future<Output = Result<DiscoverResult, McpError>> + MaybeSendFuture + '_ {
539+
(**self).discover(context)
540+
}
541+
482542
fn complete(
483543
&self,
484544
request: CompleteRequestParams,

crates/rmcp/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub use handler::server::wrapper::Json;
1919
#[cfg(any(feature = "client", feature = "server"))]
2020
pub use service::{Peer, Service, ServiceError, ServiceExt};
2121
#[cfg(feature = "client")]
22-
pub use service::{RoleClient, serve_client};
22+
pub use service::{RoleClient, select_protocol_version, serve_client};
2323
#[cfg(feature = "server")]
2424
pub use service::{RoleServer, serve_server};
2525

crates/rmcp/src/model.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,10 @@ pub struct JsonRpcNotification<N = Notification> {
524524
pub struct ErrorCode(pub i32);
525525

526526
impl ErrorCode {
527+
/// The request used a protocol version the server does not support.
528+
pub const UNSUPPORTED_PROTOCOL_VERSION: Self = Self(-32022);
529+
/// Processing the request requires a client capability that was not declared.
530+
pub const MISSING_REQUIRED_CLIENT_CAPABILITY: Self = Self(-32021);
527531
pub const HEADER_MISMATCH: Self = Self(-32020);
528532
pub const RESOURCE_NOT_FOUND: Self = Self(-32002);
529533
pub const INVALID_REQUEST: Self = Self(-32600);
@@ -573,6 +577,30 @@ impl ErrorData {
573577
pub fn header_mismatch(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
574578
Self::new(ErrorCode::HEADER_MISMATCH, message, data)
575579
}
580+
/// Create an unsupported-protocol-version error.
581+
pub fn unsupported_protocol_version(
582+
requested: ProtocolVersion,
583+
supported: &[ProtocolVersion],
584+
) -> Self {
585+
Self::new(
586+
ErrorCode::UNSUPPORTED_PROTOCOL_VERSION,
587+
"Unsupported protocol version",
588+
Some(serde_json::json!({
589+
"requested": requested,
590+
"supported": supported,
591+
})),
592+
)
593+
}
594+
/// Create a missing-required-capability error.
595+
pub fn missing_required_client_capability(required: ClientCapabilities) -> Self {
596+
Self::new(
597+
ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY,
598+
"Missing required client capability",
599+
Some(serde_json::json!({
600+
"requiredCapabilities": required,
601+
})),
602+
)
603+
}
576604
pub fn parse_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
577605
Self::new(ErrorCode::PARSE_ERROR, message, data)
578606
}
@@ -1000,6 +1028,112 @@ impl InitializeResult {
10001028
pub type ServerInfo = InitializeResult;
10011029
pub type ClientInfo = InitializeRequestParams;
10021030

1031+
const_string!(DiscoverRequestMethod = "server/discover");
1032+
1033+
/// Parameters for [`DiscoverRequest`].
1034+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
1035+
#[serde(deny_unknown_fields)]
1036+
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
1037+
pub struct DiscoverRequestParams {}
1038+
1039+
#[cfg(feature = "schemars")]
1040+
#[derive(schemars::JsonSchema)]
1041+
#[expect(dead_code, reason = "schema-only representation of request parameters")]
1042+
struct DiscoverRequestParamsSchema {
1043+
#[schemars(rename = "_meta")]
1044+
meta: RequestMetaObject,
1045+
}
1046+
1047+
#[cfg(feature = "schemars")]
1048+
impl schemars::JsonSchema for DiscoverRequestParams {
1049+
fn schema_name() -> Cow<'static, str> {
1050+
Cow::Borrowed("DiscoverRequestParams")
1051+
}
1052+
1053+
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1054+
DiscoverRequestParamsSchema::json_schema(generator)
1055+
}
1056+
}
1057+
1058+
/// A request for the server's supported protocol versions and capabilities.
1059+
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
1060+
1061+
/// The server's response to a [`DiscoverRequest`].
1062+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1063+
#[serde(rename_all = "camelCase")]
1064+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1065+
#[non_exhaustive]
1066+
pub struct DiscoverResult {
1067+
/// Identifies how the result should be parsed.
1068+
pub result_type: ResultType,
1069+
/// Protocol versions implemented by this server.
1070+
pub supported_versions: Vec<ProtocolVersion>,
1071+
/// Capabilities provided by this server.
1072+
pub capabilities: ServerCapabilities,
1073+
/// Information about the server implementation.
1074+
pub server_info: Implementation,
1075+
/// Optional guidance for using the server.
1076+
#[serde(skip_serializing_if = "Option::is_none")]
1077+
pub instructions: Option<String>,
1078+
/// How long clients may consider this response fresh, in milliseconds.
1079+
pub ttl_ms: u64,
1080+
/// Whether the cached result may be shared across authorization contexts.
1081+
pub cache_scope: CacheScope,
1082+
/// Protocol-level response metadata.
1083+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1084+
pub meta: Option<MetaObject>,
1085+
}
1086+
1087+
impl DiscoverResult {
1088+
/// Create a non-cacheable private discovery result.
1089+
pub fn new(
1090+
supported_versions: Vec<ProtocolVersion>,
1091+
capabilities: ServerCapabilities,
1092+
server_info: Implementation,
1093+
) -> Self {
1094+
Self {
1095+
result_type: ResultType::COMPLETE,
1096+
supported_versions,
1097+
capabilities,
1098+
server_info,
1099+
instructions: None,
1100+
ttl_ms: 0,
1101+
cache_scope: CacheScope::Private,
1102+
meta: None,
1103+
}
1104+
}
1105+
1106+
/// Create a discovery result from the server's initialization information.
1107+
pub fn from_server_info(
1108+
supported_versions: Vec<ProtocolVersion>,
1109+
server_info: ServerInfo,
1110+
) -> Self {
1111+
let ServerInfo {
1112+
capabilities,
1113+
server_info,
1114+
instructions,
1115+
meta,
1116+
..
1117+
} = server_info;
1118+
let mut result = Self::new(supported_versions, capabilities, server_info);
1119+
result.instructions = instructions;
1120+
result.meta = meta;
1121+
result
1122+
}
1123+
1124+
/// Set the cache lifetime hint in milliseconds.
1125+
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
1126+
self.ttl_ms = ttl_ms;
1127+
self
1128+
}
1129+
1130+
/// Set the cache scope.
1131+
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
1132+
self.cache_scope = cache_scope;
1133+
self
1134+
}
1135+
}
1136+
10031137
#[allow(clippy::derivable_impls)]
10041138
impl Default for ServerInfo {
10051139
fn default() -> Self {
@@ -3795,6 +3929,7 @@ ts_union!(
37953929
export type ClientRequest =
37963930
| PingRequest
37973931
| InitializeRequest
3932+
| DiscoverRequest
37983933
| CompleteRequest
37993934
| SetLevelRequest
38003935
| GetPromptRequest
@@ -3818,6 +3953,7 @@ impl ClientRequest {
38183953
match &self {
38193954
ClientRequest::PingRequest(r) => r.method.as_str(),
38203955
ClientRequest::InitializeRequest(r) => r.method.as_str(),
3956+
ClientRequest::DiscoverRequest(r) => r.method.as_str(),
38213957
ClientRequest::CompleteRequest(r) => r.method.as_str(),
38223958
ClientRequest::SetLevelRequest(r) => r.method.as_str(),
38233959
ClientRequest::GetPromptRequest(r) => r.method.as_str(),
@@ -3889,6 +4025,7 @@ ts_union!(
38894025

38904026
ts_union!(
38914027
export type ServerResult =
4028+
| DiscoverResult
38924029
| InitializeResult
38934030
| CompleteResult
38944031
| GetPromptResult

crates/rmcp/src/model/meta.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ variant_extension! {
189189
ClientRequest: RequestMetaObject {
190190
PingRequest
191191
InitializeRequest
192+
DiscoverRequest
192193
CompleteRequest
193194
SetLevelRequest
194195
GetPromptRequest

crates/rmcp/src/service/client.rs

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ use crate::{
1010
ArgumentInfo, CallToolRequest, CallToolRequestParams, CallToolResponse, CallToolResult,
1111
CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
1212
ClientNotification, ClientRequest, ClientResult, CompleteRequest, CompleteRequestParams,
13-
CompleteResult, CompletionContext, CompletionInfo, DEFAULT_MRTR_MAX_ROUNDS, ErrorData,
14-
GetExtensions, GetMeta, GetPromptRequest, GetPromptRequestParams, GetPromptResponse,
15-
GetPromptResult, InitializeRequest, InitializedNotification, InputRequest,
16-
InputRequiredResult, InputResponses, JsonRpcResponse, ListPromptsRequest,
17-
ListPromptsResult, ListResourceTemplatesRequest, ListResourceTemplatesResult,
18-
ListResourcesRequest, ListResourcesResult, ListToolsRequest, ListToolsResult,
19-
NumberOrString, PaginatedRequestParams, ProgressNotification, ProgressNotificationParam,
13+
CompleteResult, CompletionContext, CompletionInfo, DEFAULT_MRTR_MAX_ROUNDS,
14+
DiscoverRequest, DiscoverRequestParams, DiscoverResult, ErrorData, GetExtensions, GetMeta,
15+
GetPromptRequest, GetPromptRequestParams, GetPromptResponse, GetPromptResult,
16+
InitializeRequest, InitializedNotification, InputRequest, InputRequiredResult,
17+
InputResponses, JsonRpcResponse, ListPromptsRequest, ListPromptsResult,
18+
ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
19+
ListResourcesResult, ListToolsRequest, ListToolsResult, NumberOrString,
20+
PaginatedRequestParams, ProgressNotification, ProgressNotificationParam, ProtocolVersion,
2021
ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult,
21-
Reference, RequestId, RootsListChangedNotification, ServerInfo, ServerJsonRpcMessage,
22-
ServerNotification, ServerRequest, ServerResult, SetLevelRequest, SetLevelRequestParams,
23-
SubscribeRequest, SubscribeRequestParams, UnsubscribeRequest, UnsubscribeRequestParams,
22+
Reference, RequestId, RequestMetaObject, RootsListChangedNotification, ServerInfo,
23+
ServerJsonRpcMessage, ServerNotification, ServerRequest, ServerResult, SetLevelRequest,
24+
SetLevelRequestParams, SubscribeRequest, SubscribeRequestParams, UnsubscribeRequest,
25+
UnsubscribeRequestParams,
2426
},
2527
transport::DynamicTransportError,
2628
};
@@ -147,6 +149,19 @@ where
147149
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
148150
pub struct RoleClient;
149151

152+
/// Select the first client-preferred protocol version supported by the server.
153+
///
154+
/// Returns `None` when no version is shared.
155+
pub fn select_protocol_version(
156+
client_preference: &[ProtocolVersion],
157+
server_supported: &[ProtocolVersion],
158+
) -> Option<ProtocolVersion> {
159+
client_preference
160+
.iter()
161+
.find(|version| server_supported.contains(version))
162+
.cloned()
163+
}
164+
150165
impl ServiceRole for RoleClient {
151166
type Req = ClientRequest;
152167
type Resp = ClientResult;
@@ -363,6 +378,22 @@ macro_rules! method {
363378
}
364379

365380
impl Peer<RoleClient> {
381+
/// Discover the server's supported protocol versions and capabilities.
382+
///
383+
/// The high-level client currently exposes this peer only after initialization;
384+
/// pre-initialization probing is planned as follow-up work.
385+
pub async fn discover(&self, meta: RequestMetaObject) -> Result<DiscoverResult, ServiceError> {
386+
let mut request = DiscoverRequest::new(DiscoverRequestParams {});
387+
request.extensions.insert(meta);
388+
let result = self
389+
.send_request(ClientRequest::DiscoverRequest(request))
390+
.await?;
391+
match result {
392+
ServerResult::DiscoverResult(result) => Ok(result),
393+
_ => Err(ServiceError::UnexpectedResponse),
394+
}
395+
}
396+
366397
/// Send one `tools/call` request and return either a final result or an MRTR
367398
/// `InputRequiredResult` without driving any follow-up rounds.
368399
pub async fn call_tool_once(

0 commit comments

Comments
 (0)