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
3 changes: 2 additions & 1 deletion crates/rmcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ serde_json = "1.0"
thiserror = "2"
tokio = { version = "1", features = ["sync", "macros", "rt", "time"] }
futures = "0.3"
indexmap = { version = "2", features = ["serde"] }
tracing = { version = "0.1" }
tokio-util = { version = "0.7" }
pin-project-lite = "0.2"
Expand All @@ -61,7 +62,7 @@ oauth2 = { version = "5.0", optional = true, default-features = false }
jsonwebtoken = { version = "11", optional = true, features = ["aws_lc_rs"] }

# for auto generate schema
schemars = { version = "1.0", optional = true, features = ["chrono04"] }
schemars = { version = "1.0", optional = true, features = ["chrono04", "indexmap2"] }

# for image encoding
base64 = { version = "0.23", optional = true }
Expand Down
31 changes: 27 additions & 4 deletions crates/rmcp/src/model/elicitation_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
//! .build();
//! ```

use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData};
use std::{borrow::Cow, marker::PhantomData};

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};

use crate::{const_string, model::ConstString};
Expand Down Expand Up @@ -1123,7 +1124,7 @@ pub struct ElicitationSchema {
pub title: Option<Cow<'static, str>>,

/// Property definitions (must be primitive types)
pub properties: BTreeMap<String, PrimitiveSchemaDefinition>,
pub properties: IndexMap<String, PrimitiveSchemaDefinition>,

/// List of required property names
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -1136,7 +1137,7 @@ pub struct ElicitationSchema {

impl ElicitationSchema {
/// Create a new elicitation schema with the given properties
pub fn new(properties: BTreeMap<String, PrimitiveSchemaDefinition>) -> Self {
pub fn new(properties: IndexMap<String, PrimitiveSchemaDefinition>) -> Self {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no-local integration test still calls ElicitationSchema::new(BTreeMap::new()), so this parameter change prevents that target from compiling.

@nightcityblade nightcityblade Aug 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I updated the no-local integration test to construct an IndexMap and ran the exact no-local CI test command; it passes locally. The current head is f85271d.

Self {
type_: ObjectTypeConst,
title: None,
Expand Down Expand Up @@ -1259,7 +1260,7 @@ impl ElicitationSchema {
#[derive(Debug, Default)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct ElicitationSchemaBuilder {
pub properties: BTreeMap<String, PrimitiveSchemaDefinition>,
pub properties: IndexMap<String, PrimitiveSchemaDefinition>,
pub required: Vec<String>,
pub title: Option<Cow<'static, str>>,
pub description: Option<Cow<'static, str>>,
Expand Down Expand Up @@ -1824,6 +1825,28 @@ mod tests {
Ok(())
}

#[test]
fn test_elicitation_schema_roundtrip_preserves_property_order() -> anyhow::Result<()> {
let input = r#"{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"}}}"#;
let schema: ElicitationSchema = serde_json::from_str(input)?;

assert_eq!(
schema
.properties
.keys()
.map(String::as_str)
.collect::<Vec<_>>(),
["firstName", "lastName", "email"],
);

let output = serde_json::to_string(&schema)?;
let first_name = output.find("firstName").unwrap();
let last_name = output.find("lastName").unwrap();
let email = output.find("email").unwrap();
assert!(first_name < last_name && last_name < email);
Ok(())
}

#[test]
fn test_legacy_enum_schema_no_enum_names_omits_field() -> anyhow::Result<()> {
// `LegacyEnumSchema` with `enum_names: None` must not serialize `"enumNames": null`.
Expand Down
5 changes: 3 additions & 2 deletions crates/rmcp/tests/test_sep_2260_stream_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
//! POST's SSE stream, never the standalone GET stream.
#![cfg(not(feature = "local"))]

use std::{collections::BTreeMap, sync::Arc, time::Duration};
use std::{sync::Arc, time::Duration};

use futures::StreamExt;
use indexmap::IndexMap;
use rmcp::{
ErrorData as McpError, RoleServer, ServerHandler,
model::{
Expand Down Expand Up @@ -38,7 +39,7 @@ impl ServerHandler for ElicitingServer {
.create_elicitation(ElicitRequestParams::FormElicitationParams {
meta: None,
message: "need input".to_string(),
requested_schema: ElicitationSchema::new(BTreeMap::new()),
requested_schema: ElicitationSchema::new(IndexMap::new()),
})
.await;
Ok(CallToolResult::success(vec![ContentBlock::text("done")]).into())
Expand Down