feat: revision-2025-03-26 without streamable http (#84)
1. Suppot revision 2025-03-26 data types 2. Support meta, progress tokne, and extensions in request/notification, 3. Remove `Message`, use `JsonRpcMessage` directly
This commit is contained in:
parent
cced5e9c2f
commit
68375460df
7 changed files with 1280 additions and 229 deletions
|
|
@ -4,8 +4,8 @@ use chrono::{DateTime, Utc};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
RawContent, RawEmbeddedResource, RawImageContent, RawResource, RawResourceTemplate,
|
||||
RawTextContent, Role,
|
||||
RawAudioContent, RawContent, RawEmbeddedResource, RawImageContent, RawResource,
|
||||
RawResourceTemplate, RawTextContent, Role,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
|
|
@ -162,6 +162,7 @@ macro_rules! annotate {
|
|||
annotate!(RawContent);
|
||||
annotate!(RawTextContent);
|
||||
annotate!(RawImageContent);
|
||||
annotate!(RawAudioContent);
|
||||
annotate!(RawEmbeddedResource);
|
||||
annotate!(RawResource);
|
||||
annotate!(RawResourceTemplate);
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ pub struct ServerCapabilities {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logging: Option<JsonObject>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completions: Option<JsonObject>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompts: Option<PromptsCapability>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<ResourcesCapability>,
|
||||
|
|
@ -192,14 +194,15 @@ builder! {
|
|||
ServerCapabilities {
|
||||
experimental: ExperimentalCapabilities,
|
||||
logging: JsonObject,
|
||||
completions: JsonObject,
|
||||
prompts: PromptsCapability,
|
||||
resources: ResourcesCapability,
|
||||
tools: ToolsCapability
|
||||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const L: bool, const P: bool, const R: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, P, R, true>>
|
||||
impl<const E: bool, const L: bool, const C: bool, const P: bool, const R: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, P, R, true>>
|
||||
{
|
||||
pub fn enable_tool_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.tools.as_mut() {
|
||||
|
|
@ -209,8 +212,8 @@ impl<const E: bool, const L: bool, const P: bool, const R: bool>
|
|||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const L: bool, const R: bool, const T: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, true, R, T>>
|
||||
impl<const E: bool, const L: bool, const C: bool, const R: bool, const T: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, true, R, T>>
|
||||
{
|
||||
pub fn enable_prompts_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.prompts.as_mut() {
|
||||
|
|
@ -220,8 +223,8 @@ impl<const E: bool, const L: bool, const R: bool, const T: bool>
|
|||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const L: bool, const P: bool, const T: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, P, true, T>>
|
||||
impl<const E: bool, const L: bool, const C: bool, const P: bool, const T: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, C, P, true, T>>
|
||||
{
|
||||
pub fn enable_resources_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.resources.as_mut() {
|
||||
|
|
|
|||
|
|
@ -37,12 +37,22 @@ impl EmbeddedResource {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawAudioContent {
|
||||
pub data: String,
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
pub type AudioContent = Annotated<RawAudioContent>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum RawContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Resource(RawEmbeddedResource),
|
||||
Audio(AudioContent),
|
||||
}
|
||||
|
||||
pub type Content = Annotated<RawContent>;
|
||||
|
|
|
|||
337
crates/rmcp/src/model/extension.rs
Normal file
337
crates/rmcp/src/model/extension.rs
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
//! A container for those extra data could be carried on request or notification.
|
||||
//!
|
||||
//! This file is copied and modified from crate [http](https://github.com/hyperium/http).
|
||||
//!
|
||||
//! - Original code license: <https://github.com/hyperium/http/blob/master/LICENSE-MIT>
|
||||
//! - Original code: <https://github.com/hyperium/http/blob/master/src/extensions.rs>
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
hash::{BuildHasherDefault, Hasher},
|
||||
};
|
||||
|
||||
type AnyMap = HashMap<TypeId, Box<dyn AnyClone + Send + Sync>, BuildHasherDefault<IdHasher>>;
|
||||
|
||||
// With TypeIds as keys, there's no need to hash them. They are already hashes
|
||||
// themselves, coming from the compiler. The IdHasher just holds the u64 of
|
||||
// the TypeId, and then returns it, instead of doing any bit fiddling.
|
||||
#[derive(Default)]
|
||||
struct IdHasher(u64);
|
||||
|
||||
impl Hasher for IdHasher {
|
||||
fn write(&mut self, _: &[u8]) {
|
||||
unreachable!("TypeId calls write_u64");
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_u64(&mut self, id: u64) {
|
||||
self.0 = id;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A type map of protocol extensions.
|
||||
///
|
||||
/// `Extensions` can be used by `Request` `Notification` and `Response` to store
|
||||
/// extra data derived from the underlying protocol.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Extensions {
|
||||
// If extensions are never used, no need to carry around an empty HashMap.
|
||||
// That's 3 words. Instead, this is only 1 word.
|
||||
map: Option<Box<AnyMap>>,
|
||||
}
|
||||
|
||||
impl Extensions {
|
||||
/// Create an empty `Extensions`.
|
||||
#[inline]
|
||||
pub fn new() -> Extensions {
|
||||
Extensions { map: None }
|
||||
}
|
||||
|
||||
/// Insert a type into this `Extensions`.
|
||||
///
|
||||
/// If a extension of this type already existed, it will
|
||||
/// be returned and replaced with the new one.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// assert!(ext.insert(5i32).is_none());
|
||||
/// assert!(ext.insert(4u8).is_none());
|
||||
/// assert_eq!(ext.insert(9i32), Some(5i32));
|
||||
/// ```
|
||||
pub fn insert<T: Clone + Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
|
||||
self.map
|
||||
.get_or_insert_with(Box::default)
|
||||
.insert(TypeId::of::<T>(), Box::new(val))
|
||||
.and_then(|boxed| boxed.into_any().downcast().ok().map(|boxed| *boxed))
|
||||
}
|
||||
|
||||
/// Get a reference to a type previously inserted on this `Extensions`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// assert!(ext.get::<i32>().is_none());
|
||||
/// ext.insert(5i32);
|
||||
///
|
||||
/// assert_eq!(ext.get::<i32>(), Some(&5i32));
|
||||
/// ```
|
||||
pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
|
||||
self.map
|
||||
.as_ref()
|
||||
.and_then(|map| map.get(&TypeId::of::<T>()))
|
||||
.and_then(|boxed| (**boxed).as_any().downcast_ref())
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a type previously inserted on this `Extensions`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// ext.insert(String::from("Hello"));
|
||||
/// ext.get_mut::<String>().unwrap().push_str(" World");
|
||||
///
|
||||
/// assert_eq!(ext.get::<String>().unwrap(), "Hello World");
|
||||
/// ```
|
||||
pub fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
|
||||
self.map
|
||||
.as_mut()
|
||||
.and_then(|map| map.get_mut(&TypeId::of::<T>()))
|
||||
.and_then(|boxed| (**boxed).as_any_mut().downcast_mut())
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a type, inserting `value` if not already present on this
|
||||
/// `Extensions`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// *ext.get_or_insert(1i32) += 2;
|
||||
///
|
||||
/// assert_eq!(*ext.get::<i32>().unwrap(), 3);
|
||||
/// ```
|
||||
pub fn get_or_insert<T: Clone + Send + Sync + 'static>(&mut self, value: T) -> &mut T {
|
||||
self.get_or_insert_with(|| value)
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a type, inserting the value created by `f` if not already present
|
||||
/// on this `Extensions`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// *ext.get_or_insert_with(|| 1i32) += 2;
|
||||
///
|
||||
/// assert_eq!(*ext.get::<i32>().unwrap(), 3);
|
||||
/// ```
|
||||
pub fn get_or_insert_with<T: Clone + Send + Sync + 'static, F: FnOnce() -> T>(
|
||||
&mut self,
|
||||
f: F,
|
||||
) -> &mut T {
|
||||
let out = self
|
||||
.map
|
||||
.get_or_insert_with(Box::default)
|
||||
.entry(TypeId::of::<T>())
|
||||
.or_insert_with(|| Box::new(f()));
|
||||
(**out).as_any_mut().downcast_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a type, inserting the type's default value if not already present
|
||||
/// on this `Extensions`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// *ext.get_or_insert_default::<i32>() += 2;
|
||||
///
|
||||
/// assert_eq!(*ext.get::<i32>().unwrap(), 2);
|
||||
/// ```
|
||||
pub fn get_or_insert_default<T: Default + Clone + Send + Sync + 'static>(&mut self) -> &mut T {
|
||||
self.get_or_insert_with(T::default)
|
||||
}
|
||||
|
||||
/// Remove a type from this `Extensions`.
|
||||
///
|
||||
/// If a extension of this type existed, it will be returned.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// ext.insert(5i32);
|
||||
/// assert_eq!(ext.remove::<i32>(), Some(5i32));
|
||||
/// assert!(ext.get::<i32>().is_none());
|
||||
/// ```
|
||||
pub fn remove<T: Send + Sync + 'static>(&mut self) -> Option<T> {
|
||||
self.map
|
||||
.as_mut()
|
||||
.and_then(|map| map.remove(&TypeId::of::<T>()))
|
||||
.and_then(|boxed| boxed.into_any().downcast().ok().map(|boxed| *boxed))
|
||||
}
|
||||
|
||||
/// Clear the `Extensions` of all inserted extensions.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// ext.insert(5i32);
|
||||
/// ext.clear();
|
||||
///
|
||||
/// assert!(ext.get::<i32>().is_none());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
if let Some(ref mut map) = self.map {
|
||||
map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the extension set is empty or not.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// assert!(ext.is_empty());
|
||||
/// ext.insert(5i32);
|
||||
/// assert!(!ext.is_empty());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.as_ref().is_none_or(|map| map.is_empty())
|
||||
}
|
||||
|
||||
/// Get the number of extensions available.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext = Extensions::new();
|
||||
/// assert_eq!(ext.len(), 0);
|
||||
/// ext.insert(5i32);
|
||||
/// assert_eq!(ext.len(), 1);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.as_ref().map_or(0, |map| map.len())
|
||||
}
|
||||
|
||||
/// Extends `self` with another `Extensions`.
|
||||
///
|
||||
/// If an instance of a specific type exists in both, the one in `self` is overwritten with the
|
||||
/// one from `other`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use rmcp::model::Extensions;
|
||||
/// let mut ext_a = Extensions::new();
|
||||
/// ext_a.insert(8u8);
|
||||
/// ext_a.insert(16u16);
|
||||
///
|
||||
/// let mut ext_b = Extensions::new();
|
||||
/// ext_b.insert(4u8);
|
||||
/// ext_b.insert("hello");
|
||||
///
|
||||
/// ext_a.extend(ext_b);
|
||||
/// assert_eq!(ext_a.len(), 3);
|
||||
/// assert_eq!(ext_a.get::<u8>(), Some(&4u8));
|
||||
/// assert_eq!(ext_a.get::<u16>(), Some(&16u16));
|
||||
/// assert_eq!(ext_a.get::<&'static str>().copied(), Some("hello"));
|
||||
/// ```
|
||||
pub fn extend(&mut self, other: Self) {
|
||||
if let Some(other) = other.map {
|
||||
if let Some(map) = &mut self.map {
|
||||
map.extend(*other);
|
||||
} else {
|
||||
self.map = Some(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Extensions {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Extensions").finish()
|
||||
}
|
||||
}
|
||||
|
||||
trait AnyClone: Any {
|
||||
fn clone_box(&self) -> Box<dyn AnyClone + Send + Sync>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn into_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + Sync + 'static> AnyClone for T {
|
||||
fn clone_box(&self) -> Box<dyn AnyClone + Send + Sync> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn into_any(self: Box<Self>) -> Box<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn AnyClone + Send + Sync> {
|
||||
fn clone(&self) -> Self {
|
||||
(**self).clone_box()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extensions() {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct MyType(i32);
|
||||
|
||||
let mut extensions = Extensions::new();
|
||||
|
||||
extensions.insert(5i32);
|
||||
extensions.insert(MyType(10));
|
||||
|
||||
assert_eq!(extensions.get(), Some(&5i32));
|
||||
assert_eq!(extensions.get_mut(), Some(&mut 5i32));
|
||||
|
||||
let ext2 = extensions.clone();
|
||||
|
||||
assert_eq!(extensions.remove::<i32>(), Some(5i32));
|
||||
assert!(extensions.get::<i32>().is_none());
|
||||
|
||||
// clone still has it
|
||||
assert_eq!(ext2.get(), Some(&5i32));
|
||||
assert_eq!(ext2.get(), Some(&MyType(10)));
|
||||
|
||||
assert_eq!(extensions.get::<bool>(), None);
|
||||
assert_eq!(extensions.get(), Some(&MyType(10)));
|
||||
}
|
||||
150
crates/rmcp/src/model/meta.rs
Normal file
150
crates/rmcp/src/model/meta.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
ClientNotification, ClientRequest, Extensions, JsonObject, NumberOrString, ProgressToken,
|
||||
ServerNotification, ServerRequest,
|
||||
};
|
||||
|
||||
pub trait GetMeta {
|
||||
fn get_meta_mut(&mut self) -> &mut Meta;
|
||||
fn get_meta(&self) -> &Meta;
|
||||
}
|
||||
|
||||
macro_rules! variant_extension {
|
||||
(
|
||||
$Enum: ident {
|
||||
$($variant: ident)*
|
||||
}
|
||||
) => {
|
||||
impl $Enum {
|
||||
pub fn extensions(&self) -> &Extensions {
|
||||
match self {
|
||||
$(
|
||||
$Enum::$variant(v) => &v.extensions,
|
||||
)*
|
||||
}
|
||||
}
|
||||
pub fn extensions_mut(&mut self) -> &mut Extensions {
|
||||
match self {
|
||||
$(
|
||||
$Enum::$variant(v) => &mut v.extensions,
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
impl GetMeta for $Enum {
|
||||
fn get_meta_mut(&mut self) -> &mut Meta {
|
||||
self.extensions_mut().get_or_insert_default()
|
||||
}
|
||||
fn get_meta(&self) -> &Meta {
|
||||
self.extensions().get::<Meta>().unwrap_or(Meta::static_empty())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
variant_extension! {
|
||||
ClientRequest {
|
||||
PingRequest
|
||||
InitializeRequest
|
||||
CompleteRequest
|
||||
SetLevelRequest
|
||||
GetPromptRequest
|
||||
ListPromptsRequest
|
||||
ListResourcesRequest
|
||||
ListResourceTemplatesRequest
|
||||
ReadResourceRequest
|
||||
SubscribeRequest
|
||||
UnsubscribeRequest
|
||||
CallToolRequest
|
||||
ListToolsRequest
|
||||
}
|
||||
}
|
||||
|
||||
variant_extension! {
|
||||
ServerRequest {
|
||||
PingRequest
|
||||
CreateMessageRequest
|
||||
ListRootsRequest
|
||||
}
|
||||
}
|
||||
|
||||
variant_extension! {
|
||||
ClientNotification {
|
||||
CancelledNotification
|
||||
ProgressNotification
|
||||
InitializedNotification
|
||||
RootsListChangedNotification
|
||||
}
|
||||
}
|
||||
|
||||
variant_extension! {
|
||||
ServerNotification {
|
||||
CancelledNotification
|
||||
ProgressNotification
|
||||
LoggingMessageNotification
|
||||
ResourceUpdatedNotification
|
||||
ResourceListChangedNotification
|
||||
ToolListChangedNotification
|
||||
PromptListChangedNotification
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[serde(transparent)]
|
||||
pub struct Meta(pub JsonObject);
|
||||
const PROGRESS_TOKEN_FIELD: &str = "progressToken";
|
||||
impl Meta {
|
||||
pub fn new() -> Self {
|
||||
Self(JsonObject::new())
|
||||
}
|
||||
|
||||
pub(crate) fn static_empty() -> &'static Self {
|
||||
static EMPTY: std::sync::OnceLock<Meta> = std::sync::OnceLock::new();
|
||||
EMPTY.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
pub fn get_progress_token(&self) -> Option<ProgressToken> {
|
||||
self.0.get(PROGRESS_TOKEN_FIELD).and_then(|v| match v {
|
||||
Value::String(s) => Some(ProgressToken(NumberOrString::String(s.to_string().into()))),
|
||||
Value::Number(n) => n
|
||||
.as_u64()
|
||||
.map(|n| ProgressToken(NumberOrString::Number(n as u32))),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_progress_token(&mut self, token: ProgressToken) {
|
||||
match token.0 {
|
||||
NumberOrString::String(ref s) => self.0.insert(
|
||||
PROGRESS_TOKEN_FIELD.to_string(),
|
||||
Value::String(s.to_string()),
|
||||
),
|
||||
NumberOrString::Number(n) => self
|
||||
.0
|
||||
.insert(PROGRESS_TOKEN_FIELD.to_string(), Value::Number(n.into())),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: Meta) {
|
||||
for (k, v) in other.0.into_iter() {
|
||||
self.0.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Meta {
|
||||
type Target = JsonObject;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Meta {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
267
crates/rmcp/src/model/serde_impl.rs
Normal file
267
crates/rmcp/src/model/serde_impl.rs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
Extensions, Meta, Notification, NotificationNoParam, Request, RequestNoParam,
|
||||
RequestOptionalParam,
|
||||
};
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct WithMeta<'a, P> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
_meta: Option<Cow<'a, Meta>>,
|
||||
#[serde(flatten)]
|
||||
_rest: P,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Proxy<'a, M, P> {
|
||||
method: M,
|
||||
params: WithMeta<'a, P>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ProxyOptionalParam<'a, M, P> {
|
||||
method: M,
|
||||
params: Option<WithMeta<'a, P>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ProxyNoParam<M> {
|
||||
method: M,
|
||||
}
|
||||
|
||||
impl<M, R> Serialize for Request<M, R>
|
||||
where
|
||||
M: Serialize,
|
||||
R: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_rest: &self.params,
|
||||
_meta,
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, M, R> Deserialize<'de> for Request<M, R>
|
||||
where
|
||||
M: Deserialize<'de>,
|
||||
R: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = Proxy::deserialize(deserializer)?;
|
||||
let _meta = body.params._meta.map(|m| m.into_owned());
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
}
|
||||
Ok(Request {
|
||||
extensions,
|
||||
method: body.method,
|
||||
params: body.params._rest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, R> Serialize for RequestOptionalParam<M, R>
|
||||
where
|
||||
M: Serialize,
|
||||
R: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_rest: &self.params,
|
||||
_meta,
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, M, R> Deserialize<'de> for RequestOptionalParam<M, R>
|
||||
where
|
||||
M: Deserialize<'de>,
|
||||
R: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = ProxyOptionalParam::<'_, _, Option<R>>::deserialize(deserializer)?;
|
||||
let mut params = None;
|
||||
let mut _meta = None;
|
||||
if let Some(body_params) = body.params {
|
||||
params = body_params._rest;
|
||||
_meta = body_params._meta.map(|m| m.into_owned());
|
||||
}
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
}
|
||||
Ok(RequestOptionalParam {
|
||||
extensions,
|
||||
method: body.method,
|
||||
params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Serialize for RequestNoParam<M>
|
||||
where
|
||||
M: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
ProxyNoParam::serialize(
|
||||
&ProxyNoParam {
|
||||
method: &self.method,
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, M> Deserialize<'de> for RequestNoParam<M>
|
||||
where
|
||||
M: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = ProxyNoParam::<_>::deserialize(deserializer)?;
|
||||
let extensions = Extensions::new();
|
||||
Ok(RequestNoParam {
|
||||
extensions,
|
||||
method: body.method,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, R> Serialize for Notification<M, R>
|
||||
where
|
||||
M: Serialize,
|
||||
R: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
Proxy::serialize(
|
||||
&Proxy {
|
||||
method: &self.method,
|
||||
params: WithMeta {
|
||||
_rest: &self.params,
|
||||
_meta,
|
||||
},
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, M, R> Deserialize<'de> for Notification<M, R>
|
||||
where
|
||||
M: Deserialize<'de>,
|
||||
R: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = Proxy::deserialize(deserializer)?;
|
||||
let _meta = body.params._meta.map(|m| m.into_owned());
|
||||
let mut extensions = Extensions::new();
|
||||
if let Some(meta) = _meta {
|
||||
extensions.insert(meta);
|
||||
}
|
||||
Ok(Notification {
|
||||
extensions,
|
||||
method: body.method,
|
||||
params: body.params._rest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Serialize for NotificationNoParam<M>
|
||||
where
|
||||
M: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let extensions = &self.extensions;
|
||||
let _meta = extensions.get::<Meta>().map(Cow::Borrowed);
|
||||
ProxyNoParam::serialize(
|
||||
&ProxyNoParam {
|
||||
method: &self.method,
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, M> Deserialize<'de> for NotificationNoParam<M>
|
||||
where
|
||||
M: Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let body = ProxyNoParam::<_>::deserialize(deserializer)?;
|
||||
let extensions = Extensions::new();
|
||||
Ok(NotificationNoParam {
|
||||
extensions,
|
||||
method: body.method,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::model::ListToolsRequest;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_lost_tools_request() {
|
||||
let _req: ListToolsRequest = serde_json::from_value(json!(
|
||||
{
|
||||
"method": "tools/list",
|
||||
}
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,102 @@ pub struct Tool {
|
|||
pub description: Option<Cow<'static, str>>,
|
||||
/// A JSON Schema object defining the expected parameters for the tool
|
||||
pub input_schema: Arc<JsonObject>,
|
||||
/// Optional additional tool information.
|
||||
pub annotations: Option<ToolAnnotations>,
|
||||
}
|
||||
|
||||
/// Additional properties describing a Tool to clients.
|
||||
///
|
||||
/// NOTE: all properties in ToolAnnotations are **hints**.
|
||||
/// They are not guaranteed to provide a faithful description of
|
||||
/// tool behavior (including descriptive properties like `title`).
|
||||
///
|
||||
/// Clients should never make tool use decisions based on ToolAnnotations
|
||||
/// received from untrusted servers.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolAnnotations {
|
||||
/// A human-readable title for the tool.
|
||||
pub title: Option<String>,
|
||||
|
||||
/// If true, the tool does not modify its environment.
|
||||
///
|
||||
/// Default: false
|
||||
pub read_only_hint: Option<bool>,
|
||||
|
||||
/// If true, the tool may perform destructive updates to its environment.
|
||||
/// If false, the tool performs only additive updates.
|
||||
///
|
||||
/// (This property is meaningful only when `readOnlyHint == false`)
|
||||
///
|
||||
/// Default: true
|
||||
/// A human-readable description of the tool's purpose.
|
||||
pub destructive_hint: Option<bool>,
|
||||
|
||||
/// If true, calling the tool repeatedly with the same arguments
|
||||
/// will have no additional effect on the its environment.
|
||||
///
|
||||
/// (This property is meaningful only when `readOnlyHint == false`)
|
||||
///
|
||||
/// Default: false.
|
||||
pub idempotent_hint: Option<bool>,
|
||||
|
||||
/// If true, this tool may interact with an "open world" of external
|
||||
/// entities. If false, the tool's domain of interaction is closed.
|
||||
/// For example, the world of a web search tool is open, whereas that
|
||||
/// of a memory tool is not.
|
||||
///
|
||||
/// Default: true
|
||||
pub open_world_hint: Option<bool>,
|
||||
}
|
||||
|
||||
impl ToolAnnotations {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn with_title<T>(title: T) -> Self
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
ToolAnnotations {
|
||||
title: Some(title.into()),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn read_only(self, read_only: bool) -> Self {
|
||||
ToolAnnotations {
|
||||
read_only_hint: Some(read_only),
|
||||
..self
|
||||
}
|
||||
}
|
||||
pub fn destructive(self, destructive: bool) -> Self {
|
||||
ToolAnnotations {
|
||||
destructive_hint: Some(destructive),
|
||||
..self
|
||||
}
|
||||
}
|
||||
pub fn idempotent(self, idempotent: bool) -> Self {
|
||||
ToolAnnotations {
|
||||
idempotent_hint: Some(idempotent),
|
||||
..self
|
||||
}
|
||||
}
|
||||
pub fn open_world(self, open_world: bool) -> Self {
|
||||
ToolAnnotations {
|
||||
open_world_hint: Some(open_world),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// If not set, defaults to true.
|
||||
pub fn is_destructive(&self) -> bool {
|
||||
self.destructive_hint.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// If not set, defaults to false.
|
||||
pub fn is_idempotent(&self) -> bool {
|
||||
self.idempotent_hint.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
|
|
@ -32,6 +128,14 @@ impl Tool {
|
|||
name: name.into(),
|
||||
description: Some(description.into()),
|
||||
input_schema: input_schema.into(),
|
||||
annotations: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn annotate(self, annotations: ToolAnnotations) -> Self {
|
||||
Tool {
|
||||
annotations: Some(annotations),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue