Move whole rmcp crate to offcial rust sdk (#44)
* get rid of async-trait * move rmcp to official-rust-sdk * replace master with main * update readme, change license * fix typo * Update README.md Co-authored-by: Jefry Dewangga <jefrydco@gmail.com> * fix typos --------- Co-authored-by: = <=> Co-authored-by: Jefry Dewangga <jefrydco@gmail.com>
This commit is contained in:
parent
31493cfbcc
commit
0be6067326
6 changed files with 8676 additions and 4466 deletions
220
crates/rmcp/src/model/annotated.rs
Normal file
220
crates/rmcp/src/model/annotated.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::{
|
||||
RawContent, RawEmbeddedResource, RawImageContent, RawResource, RawResourceTemplate,
|
||||
RawTextContent, Role,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Annotations {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audience: Option<Vec<Role>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Annotations {
|
||||
/// Creates a new Annotations instance specifically for resources
|
||||
/// optional priority, and a timestamp (defaults to now if None)
|
||||
pub fn for_resource(priority: f32, timestamp: DateTime<Utc>) -> Self {
|
||||
assert!(
|
||||
(0.0..=1.0).contains(&priority),
|
||||
"Priority {priority} must be between 0.0 and 1.0"
|
||||
);
|
||||
Annotations {
|
||||
priority: Some(priority),
|
||||
timestamp: Some(timestamp),
|
||||
audience: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Annotated<T: AnnotateAble> {
|
||||
#[serde(flatten)]
|
||||
pub raw: T,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<Annotations>,
|
||||
}
|
||||
|
||||
impl<T: AnnotateAble> Deref for Annotated<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AnnotateAble> DerefMut for Annotated<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.raw
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AnnotateAble> Annotated<T> {
|
||||
pub fn new(raw: T, annotations: Option<Annotations>) -> Self {
|
||||
Self { raw, annotations }
|
||||
}
|
||||
pub fn remove_annotation(&mut self) -> Option<Annotations> {
|
||||
self.annotations.take()
|
||||
}
|
||||
pub fn audience(&self) -> Option<&Vec<Role>> {
|
||||
self.annotations.as_ref().and_then(|a| a.audience.as_ref())
|
||||
}
|
||||
pub fn priority(&self) -> Option<f32> {
|
||||
self.annotations.as_ref().and_then(|a| a.priority)
|
||||
}
|
||||
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
|
||||
self.annotations.as_ref().and_then(|a| a.timestamp)
|
||||
}
|
||||
pub fn with_audience(self, audience: Vec<Role>) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(annotations) = self.annotations {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
audience: Some(audience),
|
||||
..annotations
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
audience: Some(audience),
|
||||
priority: None,
|
||||
timestamp: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn with_priority(self, priority: f32) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(annotations) = self.annotations {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
priority: Some(priority),
|
||||
..annotations
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
priority: Some(priority),
|
||||
timestamp: None,
|
||||
audience: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn with_timestamp(self, timestamp: DateTime<Utc>) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if let Some(annotations) = self.annotations {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
timestamp: Some(timestamp),
|
||||
..annotations
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Annotated {
|
||||
raw: self.raw,
|
||||
annotations: Some(Annotations {
|
||||
timestamp: Some(timestamp),
|
||||
priority: None,
|
||||
audience: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn with_timestamp_now(self) -> Annotated<T>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.with_timestamp(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
macro_rules! annotate {
|
||||
($T: ident) => {
|
||||
impl sealed::Sealed for $T {}
|
||||
impl AnnotateAble for $T {}
|
||||
};
|
||||
}
|
||||
|
||||
annotate!(RawContent);
|
||||
annotate!(RawTextContent);
|
||||
annotate!(RawImageContent);
|
||||
annotate!(RawEmbeddedResource);
|
||||
annotate!(RawResource);
|
||||
annotate!(RawResourceTemplate);
|
||||
|
||||
pub trait AnnotateAble: sealed::Sealed {
|
||||
fn optional_annotate(self, annotations: Option<Annotations>) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Annotated::new(self, annotations)
|
||||
}
|
||||
fn annotate(self, annotations: Annotations) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Annotated::new(self, Some(annotations))
|
||||
}
|
||||
fn no_annotation(self) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Annotated::new(self, None)
|
||||
}
|
||||
fn with_audience(self, audience: Vec<Role>) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.annotate(Annotations {
|
||||
audience: Some(audience),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn with_priority(self, priority: f32) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.annotate(Annotations {
|
||||
priority: Some(priority),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn with_timestamp(self, timestamp: DateTime<Utc>) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.annotate(Annotations {
|
||||
timestamp: Some(timestamp),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn with_timestamp_now(self) -> Annotated<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.with_timestamp(Utc::now())
|
||||
}
|
||||
}
|
||||
300
crates/rmcp/src/model/capabilities.rs
Normal file
300
crates/rmcp/src/model/capabilities.rs
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
use super::JsonObject;
|
||||
use paste::paste;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::marker::PhantomData;
|
||||
pub type ExperimentalCapabilities = BTreeMap<String, JsonObject>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PromptsCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourcesCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subscribe: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolsCapability {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub list_changed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RootsCapabilities {
|
||||
pub list_changed: Option<bool>,
|
||||
}
|
||||
|
||||
///
|
||||
/// # Builder
|
||||
/// ```rust
|
||||
/// # use rmcp::model::ClientCapabilities;
|
||||
/// let cap = ClientCapabilities::builder()
|
||||
/// .enable_experimental()
|
||||
/// .enable_roots()
|
||||
/// .enable_roots_list_changed()
|
||||
/// .build();
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
pub struct ClientCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experimental: Option<ExperimentalCapabilities>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub roots: Option<RootsCapabilities>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sampling: Option<JsonObject>,
|
||||
}
|
||||
|
||||
///
|
||||
/// ## Builder
|
||||
/// ```rust
|
||||
/// # use rmcp::model::ServerCapabilities;
|
||||
/// let cap = ServerCapabilities::builder()
|
||||
/// .enable_logging()
|
||||
/// .enable_experimental()
|
||||
/// .enable_prompts()
|
||||
/// .enable_resources()
|
||||
/// .enable_tools()
|
||||
/// .enable_tool_list_changed()
|
||||
/// .build();
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerCapabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub experimental: Option<ExperimentalCapabilities>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logging: Option<JsonObject>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompts: Option<PromptsCapability>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resources: Option<ResourcesCapability>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<ToolsCapability>,
|
||||
}
|
||||
|
||||
macro_rules! builder {
|
||||
($Target: ident {$($f: ident: $T: ty),* $(,)?}) => {
|
||||
paste! {
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct [<$Target BuilderState>]<
|
||||
$(const [<$f:upper>]: bool = false,)*
|
||||
>;
|
||||
#[derive(Debug, Default)]
|
||||
pub struct [<$Target Builder>]<S = [<$Target BuilderState>]> {
|
||||
$(pub $f: Option<$T>,)*
|
||||
pub state: PhantomData<S>
|
||||
}
|
||||
impl $Target {
|
||||
#[doc = "Create a new [`" $Target "`] builder."]
|
||||
pub fn builder() -> [<$Target Builder>] {
|
||||
<[<$Target Builder>]>::default()
|
||||
}
|
||||
}
|
||||
impl<S> [<$Target Builder>]<S> {
|
||||
pub fn build(self) -> $Target {
|
||||
$Target {
|
||||
$( $f: self.$f, )*
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<S> From<[<$Target Builder>]<S>> for $Target {
|
||||
fn from(builder: [<$Target Builder>]<S>) -> Self {
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
builder!($Target @toggle $($f: $T,) *);
|
||||
|
||||
};
|
||||
($Target: ident @toggle $f0: ident: $T0: ty, $($f: ident: $T: ty,)*) => {
|
||||
builder!($Target @toggle [][$f0: $T0][$($f: $T,)*]);
|
||||
};
|
||||
($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$fn: ident: $TN: ty][$fn_1: ident: $Tn_1: ty, $($ft: ident: $Tt: ty,)*]) => {
|
||||
builder!($Target @impl_toggle [$($ff: $Tf,)*][$fn: $TN][$fn_1: $Tn_1, $($ft:$Tt,)*]);
|
||||
builder!($Target @toggle [$($ff: $Tf,)* $fn: $TN,][$fn_1: $Tn_1][$($ft:$Tt,)*]);
|
||||
};
|
||||
($Target: ident @toggle [$($ff: ident: $Tf: ty,)*][$fn: ident: $TN: ty][]) => {
|
||||
builder!($Target @impl_toggle [$($ff: $Tf,)*][$fn: $TN][]);
|
||||
};
|
||||
($Target: ident @impl_toggle [$($ff: ident: $Tf: ty,)*][$fn: ident: $TN: ty][$($ft: ident: $Tt: ty,)*]) => {
|
||||
paste! {
|
||||
impl<
|
||||
$(const [<$ff:upper>]: bool,)*
|
||||
$(const [<$ft:upper>]: bool,)*
|
||||
> [<$Target Builder>]<[<$Target BuilderState>]<
|
||||
$([<$ff:upper>],)*
|
||||
false,
|
||||
$([<$ft:upper>],)*
|
||||
>> {
|
||||
pub fn [<enable_ $fn>](self) -> [<$Target Builder>]<[<$Target BuilderState>]<
|
||||
$([<$ff:upper>],)*
|
||||
true,
|
||||
$([<$ft:upper>],)*
|
||||
>> {
|
||||
[<$Target Builder>] {
|
||||
$( $ff: self.$ff, )*
|
||||
$fn: Some($TN::default()),
|
||||
$( $ft: self.$ft, )*
|
||||
state: PhantomData
|
||||
}
|
||||
}
|
||||
pub fn [<enable_ $fn _with>](self, $fn: $TN) -> [<$Target Builder>]<[<$Target BuilderState>]<
|
||||
$([<$ff:upper>],)*
|
||||
true,
|
||||
$([<$ft:upper>],)*
|
||||
>> {
|
||||
[<$Target Builder>] {
|
||||
$( $ff: self.$ff, )*
|
||||
$fn: Some($fn),
|
||||
$( $ft: self.$ft, )*
|
||||
state: PhantomData
|
||||
}
|
||||
}
|
||||
}
|
||||
// do we really need to disable some thing in builder?
|
||||
// impl<
|
||||
// $(const [<$ff:upper>]: bool,)*
|
||||
// $(const [<$ft:upper>]: bool,)*
|
||||
// > [<$Target Builder>]<[<$Target BuilderState>]<
|
||||
// $([<$ff:upper>],)*
|
||||
// true,
|
||||
// $([<$ft:upper>],)*
|
||||
// >> {
|
||||
// pub fn [<disable_ $fn>](self) -> [<$Target Builder>]<[<$Target BuilderState>]<
|
||||
// $([<$ff:upper>],)*
|
||||
// false,
|
||||
// $([<$ft:upper>],)*
|
||||
// >> {
|
||||
// [<$Target Builder>] {
|
||||
// $( $ff: self.$ff, )*
|
||||
// $fn: None,
|
||||
// $( $ft: self.$ft, )*
|
||||
// state: PhantomData
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder! {
|
||||
ServerCapabilities {
|
||||
experimental: ExperimentalCapabilities,
|
||||
logging: 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>>
|
||||
{
|
||||
pub fn enable_tool_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.tools.as_mut() {
|
||||
c.list_changed = Some(true);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const L: bool, const R: bool, const T: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, true, R, T>>
|
||||
{
|
||||
pub fn enable_prompts_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.prompts.as_mut() {
|
||||
c.list_changed = Some(true);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const L: bool, const P: bool, const T: bool>
|
||||
ServerCapabilitiesBuilder<ServerCapabilitiesBuilderState<E, L, P, true, T>>
|
||||
{
|
||||
pub fn enable_resources_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.resources.as_mut() {
|
||||
c.list_changed = Some(true);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn enable_resources_subscribe(mut self) -> Self {
|
||||
if let Some(c) = self.resources.as_mut() {
|
||||
c.subscribe = Some(true);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
builder! {
|
||||
ClientCapabilities{
|
||||
experimental: ExperimentalCapabilities,
|
||||
roots: RootsCapabilities,
|
||||
sampling: JsonObject,
|
||||
}
|
||||
}
|
||||
|
||||
impl<const E: bool, const S: bool>
|
||||
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, true, S>>
|
||||
{
|
||||
pub fn enable_roots_list_changed(mut self) -> Self {
|
||||
if let Some(c) = self.roots.as_mut() {
|
||||
c.list_changed = Some(true);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_builder() {
|
||||
let builder = <ServerCapabilitiesBuilder>::default()
|
||||
.enable_logging()
|
||||
.enable_experimental()
|
||||
.enable_prompts()
|
||||
.enable_resources()
|
||||
.enable_tools()
|
||||
.enable_tool_list_changed();
|
||||
assert_eq!(builder.logging, Some(JsonObject::default()));
|
||||
assert_eq!(builder.prompts, Some(PromptsCapability::default()));
|
||||
assert_eq!(builder.resources, Some(ResourcesCapability::default()));
|
||||
assert_eq!(
|
||||
builder.tools,
|
||||
Some(ToolsCapability {
|
||||
list_changed: Some(true),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
builder.experimental,
|
||||
Some(ExperimentalCapabilities::default())
|
||||
);
|
||||
let client_builder = <ClientCapabilitiesBuilder>::default()
|
||||
.enable_experimental()
|
||||
.enable_roots()
|
||||
.enable_roots_list_changed()
|
||||
.enable_sampling();
|
||||
assert_eq!(
|
||||
client_builder.experimental,
|
||||
Some(ExperimentalCapabilities::default())
|
||||
);
|
||||
assert_eq!(
|
||||
client_builder.roots,
|
||||
Some(RootsCapabilities {
|
||||
list_changed: Some(true),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
152
crates/rmcp/src/model/content.rs
Normal file
152
crates/rmcp/src/model/content.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! Content sent around agents, extensions, and LLMs
|
||||
//! The various content types can be display to humans but also understood by models
|
||||
//! They include optional annotations used to help inform agent usage
|
||||
use super::resource::ResourceContents;
|
||||
use super::{AnnotateAble, Annotated};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawTextContent {
|
||||
pub text: String,
|
||||
}
|
||||
pub type TextContent = Annotated<RawTextContent>;
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawImageContent {
|
||||
/// The base64-encoded image
|
||||
pub data: String,
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
pub type ImageContent = Annotated<RawImageContent>;
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawEmbeddedResource {
|
||||
pub resource: ResourceContents,
|
||||
}
|
||||
pub type EmbeddedResource = Annotated<RawEmbeddedResource>;
|
||||
|
||||
impl EmbeddedResource {
|
||||
pub fn get_text(&self) -> String {
|
||||
match &self.resource {
|
||||
ResourceContents::TextResourceContents { text, .. } => text.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum RawContent {
|
||||
Text(RawTextContent),
|
||||
Image(RawImageContent),
|
||||
Resource(RawEmbeddedResource),
|
||||
}
|
||||
|
||||
pub type Content = Annotated<RawContent>;
|
||||
|
||||
impl RawContent {
|
||||
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::Error> {
|
||||
let json = serde_json::to_string(&json).map_err(|e| {
|
||||
crate::Error::internal_error(
|
||||
"fail to serialize response to json",
|
||||
Some(json!(
|
||||
{"reason": e.to_string()}
|
||||
)),
|
||||
)
|
||||
})?;
|
||||
Ok(RawContent::text(json))
|
||||
}
|
||||
|
||||
pub fn text<S: Into<String>>(text: S) -> Self {
|
||||
RawContent::Text(RawTextContent { text: text.into() })
|
||||
}
|
||||
|
||||
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
|
||||
RawContent::Image(RawImageContent {
|
||||
data: data.into(),
|
||||
mime_type: mime_type.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resource(resource: ResourceContents) -> Self {
|
||||
RawContent::Resource(RawEmbeddedResource { resource })
|
||||
}
|
||||
|
||||
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
||||
RawContent::Resource(RawEmbeddedResource {
|
||||
resource: ResourceContents::TextResourceContents {
|
||||
uri: uri.into(),
|
||||
mime_type: Some("text".to_string()),
|
||||
text: content.into(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the text content if this is a TextContent variant
|
||||
pub fn as_text(&self) -> Option<&RawTextContent> {
|
||||
match self {
|
||||
RawContent::Text(text) => Some(text),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the image content if this is an ImageContent variant
|
||||
pub fn as_image(&self) -> Option<&RawImageContent> {
|
||||
match self {
|
||||
RawContent::Image(image) => Some(image),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resource content if this is an ImageContent variant
|
||||
pub fn as_resource(&self) -> Option<&RawEmbeddedResource> {
|
||||
match self {
|
||||
RawContent::Resource(resource) => Some(resource),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn text<S: Into<String>>(text: S) -> Self {
|
||||
RawContent::text(text).no_annotation()
|
||||
}
|
||||
|
||||
pub fn image<S: Into<String>, T: Into<String>>(data: S, mime_type: T) -> Self {
|
||||
RawContent::image(data, mime_type).no_annotation()
|
||||
}
|
||||
|
||||
pub fn resource(resource: ResourceContents) -> Self {
|
||||
RawContent::resource(resource).no_annotation()
|
||||
}
|
||||
|
||||
pub fn embedded_text<S: Into<String>, T: Into<String>>(uri: S, content: T) -> Self {
|
||||
RawContent::embedded_text(uri, content).no_annotation()
|
||||
}
|
||||
|
||||
pub fn json<S: Serialize>(json: S) -> Result<Self, crate::Error> {
|
||||
RawContent::json(json).map(|c| c.no_annotation())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct JsonContent<S: Serialize>(S);
|
||||
/// Types that can be converted into a list of contents
|
||||
pub trait IntoContents {
|
||||
fn into_contents(self) -> Vec<Content>;
|
||||
}
|
||||
|
||||
impl IntoContents for Content {
|
||||
fn into_contents(self) -> Vec<Content> {
|
||||
vec![self]
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoContents for String {
|
||||
fn into_contents(self) -> Vec<Content> {
|
||||
vec![Content::text(self)]
|
||||
}
|
||||
}
|
||||
162
crates/rmcp/src/model/prompt.rs
Normal file
162
crates/rmcp/src/model/prompt.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
use super::content::{EmbeddedResource, ImageContent};
|
||||
use super::resource::ResourceContents;
|
||||
use super::{AnnotateAble, Annotations, RawEmbeddedResource, RawImageContent};
|
||||
use base64::engine::{Engine, general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A prompt that can be used to generate text from a model
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Prompt {
|
||||
/// The name of the prompt
|
||||
pub name: String,
|
||||
/// Optional description of what the prompt does
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Optional arguments that can be passed to customize the prompt
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arguments: Option<Vec<PromptArgument>>,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
/// Create a new prompt with the given name, description and arguments
|
||||
pub fn new<N, D>(
|
||||
name: N,
|
||||
description: Option<D>,
|
||||
arguments: Option<Vec<PromptArgument>>,
|
||||
) -> Self
|
||||
where
|
||||
N: Into<String>,
|
||||
D: Into<String>,
|
||||
{
|
||||
Prompt {
|
||||
name: name.into(),
|
||||
description: description.map(Into::into),
|
||||
arguments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a prompt argument that can be passed to customize the prompt
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PromptArgument {
|
||||
/// The name of the argument
|
||||
pub name: String,
|
||||
/// A description of what the argument is used for
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Whether this argument is required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<bool>,
|
||||
}
|
||||
|
||||
/// Represents the role of a message sender in a prompt conversation
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PromptMessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
/// Content types that can be included in prompt messages
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PromptMessageContent {
|
||||
/// Plain text content
|
||||
Text { text: String },
|
||||
/// Image content with base64-encoded data
|
||||
Image {
|
||||
#[serde(flatten)]
|
||||
image: ImageContent,
|
||||
},
|
||||
/// Embedded server-side resource
|
||||
Resource { resource: EmbeddedResource },
|
||||
}
|
||||
|
||||
impl PromptMessageContent {
|
||||
pub fn text(text: impl Into<String>) -> Self {
|
||||
Self::Text { text: text.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A message in a prompt conversation
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PromptMessage {
|
||||
/// The role of the message sender
|
||||
pub role: PromptMessageRole,
|
||||
/// The content of the message
|
||||
pub content: PromptMessageContent,
|
||||
}
|
||||
|
||||
impl PromptMessage {
|
||||
/// Create a new text message with the given role and text content
|
||||
pub fn new_text<S: Into<String>>(role: PromptMessageRole, text: S) -> Self {
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Text { text: text.into() },
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "base64")]
|
||||
pub fn new_image(
|
||||
role: PromptMessageRole,
|
||||
data: &[u8],
|
||||
mime_type: &str,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
let mime_type = mime_type.into();
|
||||
|
||||
let base64 = BASE64_STANDARD.encode(data);
|
||||
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Image {
|
||||
image: RawImageContent {
|
||||
data: base64,
|
||||
mime_type,
|
||||
}
|
||||
.optional_annotate(annotations),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resource message
|
||||
pub fn new_resource(
|
||||
role: PromptMessageRole,
|
||||
uri: String,
|
||||
mime_type: String,
|
||||
text: Option<String>,
|
||||
annotations: Option<Annotations>,
|
||||
) -> Self {
|
||||
let resource_contents = ResourceContents::TextResourceContents {
|
||||
uri,
|
||||
mime_type: Some(mime_type),
|
||||
text: text.unwrap_or_default(),
|
||||
};
|
||||
|
||||
Self {
|
||||
role,
|
||||
content: PromptMessageContent::Resource {
|
||||
resource: RawEmbeddedResource {
|
||||
resource: resource_contents,
|
||||
}
|
||||
.optional_annotate(annotations),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A template for a prompt
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PromptTemplate {
|
||||
pub id: String,
|
||||
pub template: String,
|
||||
pub arguments: Vec<PromptArgumentTemplate>,
|
||||
}
|
||||
|
||||
/// A template for a prompt argument, this should be identical to PromptArgument
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PromptArgumentTemplate {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub required: Option<bool>,
|
||||
}
|
||||
80
crates/rmcp/src/model/resource.rs
Normal file
80
crates/rmcp/src/model/resource.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Annotated;
|
||||
|
||||
/// Represents a resource in the extension with metadata
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawResource {
|
||||
/// URI representing the resource location (e.g., "file:///path/to/file" or "str:///content")
|
||||
pub uri: String,
|
||||
/// Name of the resource
|
||||
pub name: String,
|
||||
/// Optional description of the resource
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// MIME type of the resource content ("text" or "blob")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
|
||||
/// The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
||||
///
|
||||
/// This can be used by Hosts to display file sizes and estimate context window us
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<u32>,
|
||||
}
|
||||
|
||||
pub type Resource = Annotated<RawResource>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawResourceTemplate {
|
||||
pub uri_template: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
pub type ResourceTemplate = Annotated<RawResourceTemplate>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum ResourceContents {
|
||||
TextResourceContents {
|
||||
uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
mime_type: Option<String>,
|
||||
text: String,
|
||||
},
|
||||
BlobResourceContents {
|
||||
uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
mime_type: Option<String>,
|
||||
blob: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ResourceContents {
|
||||
pub fn text(text: impl Into<String>, uri: impl Into<String>) -> Self {
|
||||
Self::TextResourceContents {
|
||||
uri: uri.into(),
|
||||
mime_type: Some("text".into()),
|
||||
text: text.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawResource {
|
||||
/// Creates a new Resource from a URI with explicit mime type
|
||||
pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
uri: uri.into(),
|
||||
name: name.into(),
|
||||
description: None,
|
||||
mime_type: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
41
crates/rmcp/src/model/tool.rs
Normal file
41
crates/rmcp/src/model/tool.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
/// Tools represent a routine that a server can execute
|
||||
/// Tool calls represent requests from the client to execute one
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::JsonObject;
|
||||
|
||||
/// A tool that can be used by a model.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tool {
|
||||
/// The name of the tool
|
||||
pub name: Cow<'static, str>,
|
||||
/// A description of what the tool does
|
||||
pub description: Cow<'static, str>,
|
||||
/// A JSON Schema object defining the expected parameters for the tool
|
||||
pub input_schema: Arc<JsonObject>,
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
/// Create a new tool with the given name and description
|
||||
pub fn new<N, D, S>(name: N, description: D, input_schema: S) -> Self
|
||||
where
|
||||
N: Into<Cow<'static, str>>,
|
||||
D: Into<Cow<'static, str>>,
|
||||
S: Into<Arc<JsonObject>>,
|
||||
{
|
||||
Tool {
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
input_schema: input_schema.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the schema as json value
|
||||
pub fn schema_as_json_value(&self) -> Value {
|
||||
Value::Object(self.input_schema.as_ref().clone())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue