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
80 changes: 79 additions & 1 deletion crates/rmcp/src/model/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};

use super::{
AnnotateAble, Annotations, Icon, Meta, RawEmbeddedResource,
content::{EmbeddedResource, ImageContent},
content::{AudioContent, EmbeddedResource, ImageContent},
resource::ResourceContents,
};

Expand Down Expand Up @@ -157,6 +157,11 @@ pub enum PromptMessageContent {
#[serde(flatten)]
image: ImageContent,
},
/// Audio content with base64-encoded data
Audio {
#[serde(flatten)]
audio: AudioContent,
},
/// Embedded server-side resource
Resource {
#[serde(flatten)]
Expand Down Expand Up @@ -230,6 +235,29 @@ impl PromptMessage {
}
}

/// Create a new audio message. `annotations` is optional.
#[cfg(feature = "base64")]
pub fn new_audio(
role: PromptMessageRole,
data: &[u8],
mime_type: &str,
annotations: Option<Annotations>,
) -> Self {
use base64::{Engine, prelude::BASE64_STANDARD};

let base64 = BASE64_STANDARD.encode(data);
Self {
role,
content: PromptMessageContent::Audio {
audio: crate::model::RawAudioContent {
data: base64,
mime_type: mime_type.into(),
}
.optional_annotate(annotations),
},
}
}

/// Create a new resource message. `resource_meta`, `resource_content_meta`, and `annotations` are optional.
pub fn new_resource(
role: PromptMessageRole,
Expand Down Expand Up @@ -307,6 +335,56 @@ mod tests {
assert!(!json.contains("mime_type"));
}

#[test]
fn test_prompt_message_audio_serialization_and_deserialization() {
// Audio is part of the spec's ContentBlock union for prompt messages
// (text | image | audio | resource_link | resource). Ensure the Audio
// variant serializes to the flat, spec-compliant shape
// `{ "type": "audio", "data", "mimeType" }` and parses back.
// See: https://modelcontextprotocol.io/specification/2025-06-18/server/prompts
let content = PromptMessageContent::Audio {
audio: crate::model::RawAudioContent {
data: "YXVkaW8=".to_string(),
mime_type: "audio/wav".to_string(),
}
.no_annotation(),
};

let value = serde_json::to_value(&content).unwrap();
assert_eq!(value.get("type").and_then(|v| v.as_str()), Some("audio"));
assert_eq!(value.get("data").and_then(|v| v.as_str()), Some("YXVkaW8="));
assert_eq!(
value.get("mimeType").and_then(|v| v.as_str()),
Some("audio/wav"),
"expected camelCase mimeType, got: {value:#?}"
);

// Regression: a spec-valid audio content block must deserialize into
// the Audio variant (previously failed with "unknown variant `audio`").
let json = r#"{"type":"audio","data":"YXVkaW8=","mimeType":"audio/wav"}"#;
let parsed: PromptMessageContent = serde_json::from_str(json).unwrap();
assert_eq!(parsed, content);
}

#[test]
#[cfg(feature = "base64")]
fn test_prompt_message_new_audio_constructor() {
let message =
PromptMessage::new_audio(PromptMessageRole::User, b"hello", "audio/wav", None);
let value = serde_json::to_value(&message).unwrap();
let content = value.get("content").expect("content present");
assert_eq!(content.get("type").and_then(|v| v.as_str()), Some("audio"));
assert_eq!(
content.get("mimeType").and_then(|v| v.as_str()),
Some("audio/wav")
);
// base64 of "hello"
assert_eq!(
content.get("data").and_then(|v| v.as_str()),
Some("aGVsbG8=")
);
}

#[test]
fn test_prompt_message_resource_link_serialization() {
use super::super::resource::RawResource;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,37 @@
"mimeType"
]
},
{
"description": "Audio content with base64-encoded data",
"type": "object",
"properties": {
"annotations": {
"anyOf": [
{
"$ref": "#/definitions/Annotations"
},
{
"type": "null"
}
]
},
"data": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"type": {
"type": "string",
"const": "audio"
}
},
"required": [
"type",
"data",
"mimeType"
]
},
{
"description": "Embedded server-side resource",
"type": "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,37 @@
"mimeType"
]
},
{
"description": "Audio content with base64-encoded data",
"type": "object",
"properties": {
"annotations": {
"anyOf": [
{
"$ref": "#/definitions/Annotations"
},
{
"type": "null"
}
]
},
"data": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"type": {
"type": "string",
"const": "audio"
}
},
"required": [
"type",
"data",
"mimeType"
]
},
{
"description": "Embedded server-side resource",
"type": "object",
Expand Down