Skip to content

Commit 659e81c

Browse files
committed
feat!: add server discovery and negotiation
1 parent 98bb263 commit 659e81c

13 files changed

Lines changed: 1299 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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,10 @@ pub struct JsonRpcNotification<N = Notification> {
519519
pub struct ErrorCode(pub i32);
520520

521521
impl ErrorCode {
522+
/// The request used a protocol version the server does not support.
523+
pub const UNSUPPORTED_PROTOCOL_VERSION: Self = Self(-32022);
524+
/// Processing the request requires a client capability that was not declared.
525+
pub const MISSING_REQUIRED_CLIENT_CAPABILITY: Self = Self(-32021);
522526
pub const HEADER_MISMATCH: Self = Self(-32020);
523527
pub const RESOURCE_NOT_FOUND: Self = Self(-32002);
524528
pub const INVALID_REQUEST: Self = Self(-32600);
@@ -568,6 +572,30 @@ impl ErrorData {
568572
pub fn header_mismatch(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
569573
Self::new(ErrorCode::HEADER_MISMATCH, message, data)
570574
}
575+
/// Create an unsupported-protocol-version error.
576+
pub fn unsupported_protocol_version(
577+
requested: ProtocolVersion,
578+
supported: &[ProtocolVersion],
579+
) -> Self {
580+
Self::new(
581+
ErrorCode::UNSUPPORTED_PROTOCOL_VERSION,
582+
"Unsupported protocol version",
583+
Some(serde_json::json!({
584+
"requested": requested,
585+
"supported": supported,
586+
})),
587+
)
588+
}
589+
/// Create a missing-required-capability error.
590+
pub fn missing_required_client_capability(required: ClientCapabilities) -> Self {
591+
Self::new(
592+
ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY,
593+
"Missing required client capability",
594+
Some(serde_json::json!({
595+
"requiredCapabilities": required,
596+
})),
597+
)
598+
}
571599
pub fn parse_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
572600
Self::new(ErrorCode::PARSE_ERROR, message, data)
573601
}
@@ -993,6 +1021,135 @@ impl InitializeResult {
9931021
pub type ServerInfo = InitializeResult;
9941022
pub type ClientInfo = InitializeRequestParams;
9951023

1024+
const_string!(DiscoverRequestMethod = "server/discover");
1025+
1026+
/// Parameters for [`DiscoverRequest`].
1027+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
1028+
#[serde(deny_unknown_fields)]
1029+
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
1030+
pub struct DiscoverRequestParams {}
1031+
1032+
#[cfg(feature = "schemars")]
1033+
#[derive(schemars::JsonSchema)]
1034+
#[schemars(rename = "RequestMetaObject")]
1035+
#[expect(dead_code, reason = "schema-only representation of request metadata")]
1036+
struct RequestMetaObjectSchema {
1037+
#[schemars(default, rename = "progressToken", with = "ProgressToken")]
1038+
#[serde(skip_serializing_if = "Option::is_none")]
1039+
progress_token: Option<ProgressToken>,
1040+
#[schemars(rename = "io.modelcontextprotocol/protocolVersion")]
1041+
protocol_version: ProtocolVersion,
1042+
#[schemars(rename = "io.modelcontextprotocol/clientInfo")]
1043+
client_info: Implementation,
1044+
#[schemars(rename = "io.modelcontextprotocol/clientCapabilities")]
1045+
client_capabilities: ClientCapabilities,
1046+
#[schemars(
1047+
default,
1048+
rename = "io.modelcontextprotocol/logLevel",
1049+
with = "LoggingLevel"
1050+
)]
1051+
#[serde(skip_serializing_if = "Option::is_none")]
1052+
log_level: Option<LoggingLevel>,
1053+
}
1054+
1055+
#[cfg(feature = "schemars")]
1056+
#[derive(schemars::JsonSchema)]
1057+
#[expect(dead_code, reason = "schema-only representation of request parameters")]
1058+
struct DiscoverRequestParamsSchema {
1059+
#[schemars(rename = "_meta")]
1060+
meta: RequestMetaObjectSchema,
1061+
}
1062+
1063+
#[cfg(feature = "schemars")]
1064+
impl schemars::JsonSchema for DiscoverRequestParams {
1065+
fn schema_name() -> Cow<'static, str> {
1066+
Cow::Borrowed("DiscoverRequestParams")
1067+
}
1068+
1069+
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1070+
DiscoverRequestParamsSchema::json_schema(generator)
1071+
}
1072+
}
1073+
1074+
/// A request for the server's supported protocol versions and capabilities.
1075+
pub type DiscoverRequest = Request<DiscoverRequestMethod, DiscoverRequestParams>;
1076+
1077+
/// The server's response to a [`DiscoverRequest`].
1078+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1079+
#[serde(rename_all = "camelCase")]
1080+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1081+
#[non_exhaustive]
1082+
pub struct DiscoverResult {
1083+
/// Identifies how the result should be parsed.
1084+
pub result_type: ResultType,
1085+
/// Protocol versions implemented by this server.
1086+
pub supported_versions: Vec<ProtocolVersion>,
1087+
/// Capabilities provided by this server.
1088+
pub capabilities: ServerCapabilities,
1089+
/// Information about the server implementation.
1090+
pub server_info: Implementation,
1091+
/// Optional guidance for using the server.
1092+
#[serde(skip_serializing_if = "Option::is_none")]
1093+
pub instructions: Option<String>,
1094+
/// How long clients may consider this response fresh, in milliseconds.
1095+
pub ttl_ms: u64,
1096+
/// Whether the cached result may be shared across authorization contexts.
1097+
pub cache_scope: CacheScope,
1098+
/// Protocol-level response metadata.
1099+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1100+
pub meta: Option<Meta>,
1101+
}
1102+
1103+
impl DiscoverResult {
1104+
/// Create a non-cacheable private discovery result.
1105+
pub fn new(
1106+
supported_versions: Vec<ProtocolVersion>,
1107+
capabilities: ServerCapabilities,
1108+
server_info: Implementation,
1109+
) -> Self {
1110+
Self {
1111+
result_type: ResultType::COMPLETE,
1112+
supported_versions,
1113+
capabilities,
1114+
server_info,
1115+
instructions: None,
1116+
ttl_ms: 0,
1117+
cache_scope: CacheScope::Private,
1118+
meta: None,
1119+
}
1120+
}
1121+
1122+
/// Create a discovery result from the server's initialization information.
1123+
pub fn from_server_info(
1124+
supported_versions: Vec<ProtocolVersion>,
1125+
server_info: ServerInfo,
1126+
) -> Self {
1127+
let ServerInfo {
1128+
capabilities,
1129+
server_info,
1130+
instructions,
1131+
meta,
1132+
..
1133+
} = server_info;
1134+
let mut result = Self::new(supported_versions, capabilities, server_info);
1135+
result.instructions = instructions;
1136+
result.meta = meta;
1137+
result
1138+
}
1139+
1140+
/// Set the cache lifetime hint in milliseconds.
1141+
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
1142+
self.ttl_ms = ttl_ms;
1143+
self
1144+
}
1145+
1146+
/// Set the cache scope.
1147+
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
1148+
self.cache_scope = cache_scope;
1149+
self
1150+
}
1151+
}
1152+
9961153
#[allow(clippy::derivable_impls)]
9971154
impl Default for ServerInfo {
9981155
fn default() -> Self {
@@ -3788,6 +3945,7 @@ ts_union!(
37883945
export type ClientRequest =
37893946
| PingRequest
37903947
| InitializeRequest
3948+
| DiscoverRequest
37913949
| CompleteRequest
37923950
| SetLevelRequest
37933951
| GetPromptRequest
@@ -3811,6 +3969,7 @@ impl ClientRequest {
38113969
match &self {
38123970
ClientRequest::PingRequest(r) => r.method.as_str(),
38133971
ClientRequest::InitializeRequest(r) => r.method.as_str(),
3972+
ClientRequest::DiscoverRequest(r) => r.method.as_str(),
38143973
ClientRequest::CompleteRequest(r) => r.method.as_str(),
38153974
ClientRequest::SetLevelRequest(r) => r.method.as_str(),
38163975
ClientRequest::GetPromptRequest(r) => r.method.as_str(),
@@ -3882,6 +4041,7 @@ ts_union!(
38824041

38834042
ts_union!(
38844043
export type ServerResult =
4044+
| DiscoverResult
38854045
| InitializeResult
38864046
| CompleteResult
38874047
| GetPromptResult

crates/rmcp/src/model/meta.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ variant_extension! {
169169
ClientRequest {
170170
PingRequest
171171
InitializeRequest
172+
DiscoverRequest
172173
CompleteRequest
173174
SetLevelRequest
174175
GetPromptRequest

crates/rmcp/src/service/client.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ 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, Meta, NumberOrString,
20+
PaginatedRequestParams, ProgressNotification, ProgressNotificationParam, ProtocolVersion,
2021
ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult,
2122
Reference, RequestId, RootsListChangedNotification, ServerInfo, ServerJsonRpcMessage,
2223
ServerNotification, ServerRequest, ServerResult, SetLevelRequest, SetLevelRequestParams,
@@ -147,6 +148,19 @@ where
147148
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
148149
pub struct RoleClient;
149150

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

365379
impl Peer<RoleClient> {
380+
/// Discover the server's supported protocol versions and capabilities.
381+
pub async fn discover(&self, meta: Meta) -> Result<DiscoverResult, ServiceError> {
382+
let mut request = DiscoverRequest::new(DiscoverRequestParams {});
383+
request.extensions.insert(meta);
384+
let result = self
385+
.send_request(ClientRequest::DiscoverRequest(request))
386+
.await?;
387+
match result {
388+
ServerResult::DiscoverResult(result) => Ok(result),
389+
_ => Err(ServiceError::UnexpectedResponse),
390+
}
391+
}
392+
366393
/// Send one `tools/call` request and return either a final result or an MRTR
367394
/// `InputRequiredResult` without driving any follow-up rounds.
368395
pub async fn call_tool_once(

0 commit comments

Comments
 (0)