Skip to content

Commit ff590b0

Browse files
committed
Merge remote-tracking branch 'upstream/main' into remove-batch-json-rpc
2 parents 9a870ee + 7d46b39 commit ff590b0

File tree

10 files changed

+263
-3
lines changed

10 files changed

+263
-3
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,17 @@ See [oauth_support](docs/OAUTH_SUPPORT.md) for details.
119119
- [Schema](https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.ts)
120120

121121
## Related Projects
122+
123+
### Extending `rmcp`
124+
125+
- [rmcp-actix-web](https://gitlab.com/lx-industries/rmcp-actix-web) - An `actix_web` backend for `rmcp`
126+
- [rmcp-openapi](https://gitlab.com/lx-industries/rmcp-openapi) - Transform OpenAPI definition endpoints into MCP tools
127+
128+
### Built with `rmcp`
129+
122130
- [rustfs-mcp](https://github.com/rustfs/rustfs/tree/main/crates/mcp) - High-performance MCP server providing S3-compatible object storage operations for AI/LLM integration
123131
- [containerd-mcp-server](https://github.com/jokemanfire/mcp-containerd) - A containerd-based MCP server implementation
132+
- [rmcp-openapi-server](https://gitlab.com/lx-industries/rmcp-openapi/-/tree/main/crates/rmcp-openapi-server) - High-performance MCP server that exposes OpenAPI definition endpoints as MCP tools
124133

125134
## Development
126135

crates/rmcp/src/model.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,9 @@ pub struct CallToolResult {
12341234
/// Whether this result represents an error condition
12351235
#[serde(skip_serializing_if = "Option::is_none")]
12361236
pub is_error: Option<bool>,
1237+
/// Optional protocol-level metadata for this result
1238+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1239+
pub meta: Option<Meta>,
12371240
}
12381241

12391242
impl CallToolResult {
@@ -1243,6 +1246,7 @@ impl CallToolResult {
12431246
content,
12441247
structured_content: None,
12451248
is_error: Some(false),
1249+
meta: None,
12461250
}
12471251
}
12481252
/// Create an error tool result with unstructured content
@@ -1251,6 +1255,7 @@ impl CallToolResult {
12511255
content,
12521256
structured_content: None,
12531257
is_error: Some(true),
1258+
meta: None,
12541259
}
12551260
}
12561261
/// Create a successful tool result with structured content
@@ -1272,6 +1277,7 @@ impl CallToolResult {
12721277
content: vec![Content::text(value.to_string())],
12731278
structured_content: Some(value),
12741279
is_error: Some(false),
1280+
meta: None,
12751281
}
12761282
}
12771283
/// Create an error tool result with structured content
@@ -1297,6 +1303,7 @@ impl CallToolResult {
12971303
content: vec![Content::text(value.to_string())],
12981304
structured_content: Some(value),
12991305
is_error: Some(true),
1306+
meta: None,
13001307
}
13011308
}
13021309

@@ -1344,13 +1351,17 @@ impl<'de> Deserialize<'de> for CallToolResult {
13441351
structured_content: Option<Value>,
13451352
#[serde(skip_serializing_if = "Option::is_none")]
13461353
is_error: Option<bool>,
1354+
/// Accept `_meta` during deserialization
1355+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1356+
meta: Option<Meta>,
13471357
}
13481358

13491359
let helper = CallToolResultHelper::deserialize(deserializer)?;
13501360
let result = CallToolResult {
13511361
content: helper.content.unwrap_or_default(),
13521362
structured_content: helper.structured_content,
13531363
is_error: helper.is_error,
1364+
meta: helper.meta,
13541365
};
13551366

13561367
// Validate mutual exclusivity

crates/rmcp/src/model/content.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub type ImageContent = Annotated<RawImageContent>;
2727
#[serde(rename_all = "camelCase")]
2828
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2929
pub struct RawEmbeddedResource {
30+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
31+
pub meta: Option<super::Meta>,
3032
pub resource: ResourceContents,
3133
}
3234
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
@@ -88,15 +90,20 @@ impl RawContent {
8890
}
8991

9092
pub fn resource(resource: ResourceContents) -> Self {
91-
RawContent::Resource(RawEmbeddedResource { resource })
93+
RawContent::Resource(RawEmbeddedResource {
94+
meta: None,
95+
resource,
96+
})
9297
}
9398

9499
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
95100
RawContent::Resource(RawEmbeddedResource {
101+
meta: None,
96102
resource: ResourceContents::TextResourceContents {
97103
uri: uri.into(),
98104
mime_type: Some("text".to_string()),
99105
text: content.into(),
106+
meta: None,
100107
},
101108
})
102109
}

crates/rmcp/src/model/meta.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ variant_extension! {
107107
PromptListChangedNotification
108108
}
109109
}
110-
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
110+
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
111+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
111112
#[serde(transparent)]
112113
pub struct Meta(pub HashMap<String, Value>);
113114
const PROGRESS_TOKEN_FIELD: &str = "progressToken";

crates/rmcp/src/model/prompt.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,14 @@ impl PromptMessage {
155155
uri,
156156
mime_type: Some(mime_type),
157157
text: text.unwrap_or_default(),
158+
meta: None,
158159
};
159160

160161
Self {
161162
role,
162163
content: PromptMessageContent::Resource {
163164
resource: RawEmbeddedResource {
165+
meta: None,
164166
resource: resource_contents,
165167
}
166168
.optional_annotate(annotations),

crates/rmcp/src/model/resource.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use serde::{Deserialize, Serialize};
22

3-
use super::Annotated;
3+
use super::{Annotated, Meta};
44

55
/// Represents a resource in the extension with metadata
66
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@@ -56,13 +56,17 @@ pub enum ResourceContents {
5656
#[serde(skip_serializing_if = "Option::is_none")]
5757
mime_type: Option<String>,
5858
text: String,
59+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
60+
meta: Option<Meta>,
5961
},
6062
#[serde(rename_all = "camelCase")]
6163
BlobResourceContents {
6264
uri: String,
6365
#[serde(skip_serializing_if = "Option::is_none")]
6466
mime_type: Option<String>,
6567
blob: String,
68+
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
69+
meta: Option<Meta>,
6670
},
6771
}
6872

@@ -72,6 +76,7 @@ impl ResourceContents {
7276
uri: uri.into(),
7377
mime_type: Some("text".into()),
7478
text: text.into(),
79+
meta: None,
7580
}
7681
}
7782
}
@@ -121,6 +126,7 @@ mod tests {
121126
uri: "file:///test.txt".to_string(),
122127
mime_type: Some("text/plain".to_string()),
123128
text: "Hello world".to_string(),
129+
meta: None,
124130
};
125131

126132
let json = serde_json::to_string(&text_contents).unwrap();
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use rmcp::model::{AnnotateAble, Content, Meta, RawContent, ResourceContents};
2+
use serde_json::json;
3+
4+
#[test]
5+
fn serialize_embedded_text_resource_with_meta() {
6+
// Inner contents meta
7+
let mut inner_meta = Meta::new();
8+
inner_meta.insert("inner".to_string(), json!(2));
9+
10+
// Top-level embedded resource meta
11+
let mut top_meta = Meta::new();
12+
top_meta.insert("top".to_string(), json!(1));
13+
14+
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
15+
meta: Some(top_meta),
16+
resource: ResourceContents::TextResourceContents {
17+
uri: "str://example".to_string(),
18+
mime_type: Some("text/plain".to_string()),
19+
text: "hello".to_string(),
20+
meta: Some(inner_meta),
21+
},
22+
})
23+
.no_annotation();
24+
25+
let v = serde_json::to_value(&content).unwrap();
26+
27+
let expected = json!({
28+
"type": "resource",
29+
"_meta": {"top": 1},
30+
"resource": {
31+
"uri": "str://example",
32+
"mimeType": "text/plain",
33+
"text": "hello",
34+
"_meta": {"inner": 2}
35+
}
36+
});
37+
38+
assert_eq!(v, expected);
39+
}
40+
41+
#[test]
42+
fn serialize_embedded_text_resource_without_meta_omits_fields() {
43+
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
44+
meta: None,
45+
resource: ResourceContents::TextResourceContents {
46+
uri: "str://no-meta".to_string(),
47+
mime_type: Some("text/plain".to_string()),
48+
text: "hi".to_string(),
49+
meta: None,
50+
},
51+
})
52+
.no_annotation();
53+
54+
let v = serde_json::to_value(&content).unwrap();
55+
56+
assert_eq!(v.get("_meta"), None);
57+
let inner = v.get("resource").and_then(|r| r.as_object()).unwrap();
58+
assert_eq!(inner.get("_meta"), None);
59+
}
60+
61+
#[test]
62+
fn deserialize_embedded_text_resource_with_meta() {
63+
let raw = json!({
64+
"type": "resource",
65+
"_meta": {"x": true},
66+
"resource": {
67+
"uri": "str://from-json",
68+
"text": "ok",
69+
"_meta": {"y": 42}
70+
}
71+
});
72+
73+
let content: Content = serde_json::from_value(raw).unwrap();
74+
75+
let raw = match &content.raw {
76+
RawContent::Resource(er) => er,
77+
_ => panic!("expected resource"),
78+
};
79+
80+
// top-level _meta
81+
let top = raw.meta.as_ref().expect("top-level meta missing");
82+
assert_eq!(top.get("x").unwrap(), &json!(true));
83+
84+
// inner contents _meta
85+
match &raw.resource {
86+
ResourceContents::TextResourceContents {
87+
meta, uri, text, ..
88+
} => {
89+
assert_eq!(uri, "str://from-json");
90+
assert_eq!(text, "ok");
91+
let inner = meta.as_ref().expect("inner meta missing");
92+
assert_eq!(inner.get("y").unwrap(), &json!(42));
93+
}
94+
_ => panic!("expected text resource contents"),
95+
}
96+
}
97+
98+
#[test]
99+
fn serialize_embedded_blob_resource_with_meta() {
100+
let mut inner_meta = Meta::new();
101+
inner_meta.insert("blob_inner".to_string(), json!(true));
102+
103+
let mut top_meta = Meta::new();
104+
top_meta.insert("blob_top".to_string(), json!("t"));
105+
106+
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
107+
meta: Some(top_meta),
108+
resource: ResourceContents::BlobResourceContents {
109+
uri: "str://blob".to_string(),
110+
mime_type: Some("application/octet-stream".to_string()),
111+
blob: "Zm9v".to_string(),
112+
meta: Some(inner_meta),
113+
},
114+
})
115+
.no_annotation();
116+
117+
let v = serde_json::to_value(&content).unwrap();
118+
119+
assert_eq!(v.get("_meta").unwrap(), &json!({"blob_top": "t"}));
120+
let inner = v.get("resource").unwrap();
121+
assert_eq!(inner.get("_meta").unwrap(), &json!({"blob_inner": true}));
122+
}

crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,13 @@
858858
"RawEmbeddedResource": {
859859
"type": "object",
860860
"properties": {
861+
"_meta": {
862+
"type": [
863+
"object",
864+
"null"
865+
],
866+
"additionalProperties": true
867+
},
861868
"resource": {
862869
"$ref": "#/definitions/ResourceContents"
863870
}
@@ -1218,6 +1225,13 @@
12181225
{
12191226
"type": "object",
12201227
"properties": {
1228+
"_meta": {
1229+
"type": [
1230+
"object",
1231+
"null"
1232+
],
1233+
"additionalProperties": true
1234+
},
12211235
"mimeType": {
12221236
"type": [
12231237
"string",
@@ -1239,6 +1253,13 @@
12391253
{
12401254
"type": "object",
12411255
"properties": {
1256+
"_meta": {
1257+
"type": [
1258+
"object",
1259+
"null"
1260+
],
1261+
"additionalProperties": true
1262+
},
12421263
"blob": {
12431264
"type": "string"
12441265
},

0 commit comments

Comments
 (0)