rust-sdk/crates/rmcp/src/model/prompt.rs
Stanley Horwood e9a5ae9901
feat(model): add json schema generation support for all model types (#176)
* feat(model): add json schema generation support for all model types

This commit adds JSON Schema support for all model types by implementing
`schemars::JsonSchema` trait. The feature is gated behind the new
`schemars` feature flag. This enables automatic schema generation for
API documentation and validation purposes. Added tests to verify schema
generation for client and server JSON-RPC messages.

* fix(model): add manual json schema implementation for `NumberOrString`

This commit adds a manual implementation of `JsonSchema` trait for the
`NumberOrString` enum to properly represent its union type nature in
JSON Schema. The schema now correctly specifies that the type can be
either a number or a string using the `oneOf` validation keyword.

* fix(model): skip extensions field in json schema generation

The `Extensions` type was incorrectly included in JSON schema
generation, which could lead to confusing API documentation. This commit
adds `#[schemars(skip)]` attribute to all `extensions` fields in request
and notification structs, and removes the manual `JsonSchema`
implementation for the `Extensions` type since it's an internal
implementation detail that shouldn't be exposed in the schema.
2025-05-15 23:56:31 +08:00

170 lines
5 KiB
Rust

use base64::engine::{Engine, general_purpose::STANDARD as BASE64_STANDARD};
use serde::{Deserialize, Serialize};
use super::{
AnnotateAble, Annotations, RawEmbeddedResource, RawImageContent,
content::{EmbeddedResource, ImageContent},
resource::ResourceContents,
};
/// A prompt that can be used to generate text from a model
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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>,
}