use rmcp::model::{ElicitationAction, PromptArgument, PromptMessage, ResourceContents};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub use vtcode_utility_tool_specs::{
MCP_UI_EXTENSION_ID, MCP_UI_RESOURCE_MIME_TYPE, MCP_UI_RESOURCE_SCHEME, McpToolVisibility, McpUiResourceMeta,
McpUiToolMeta, parse_ui_resource_meta,
};
#[derive(Debug, Clone)]
pub struct McpToolInfo {
pub name: String,
pub description: String,
pub provider: String,
pub input_schema: Value,
pub output_schema: Option<Value>,
pub ui: Option<McpUiToolMeta>,
pub visibility: McpToolVisibility,
}
#[derive(Debug, Clone)]
pub struct McpResourceInfo {
pub provider: String,
pub uri: String,
pub name: String,
pub description: Option<String>,
pub mime_type: Option<String>,
pub size: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct McpResourceData {
pub provider: String,
pub uri: String,
pub contents: Vec<ResourceContents>,
pub meta: Map<String, Value>,
}
#[derive(Debug, Clone)]
pub struct McpPromptInfo {
pub(crate) provider: String,
pub(crate) name: String,
pub(crate) description: Option<String>,
pub(crate) arguments: Vec<PromptArgument>,
}
#[derive(Debug, Clone)]
pub struct McpPromptDetail {
pub(crate) provider: String,
pub(crate) name: String,
pub(crate) description: Option<String>,
pub(crate) messages: Vec<PromptMessage>,
pub(crate) meta: Map<String, Value>,
}
#[derive(Debug, Clone)]
pub struct McpClientStatus {
pub enabled: bool,
pub provider_count: usize,
pub active_connections: usize,
pub configured_providers: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct McpElicitationRequest {
pub message: String,
pub requested_schema: Value,
pub(crate) meta: Option<Value>,
}
#[derive(Debug, Clone)]
pub struct McpElicitationResponse {
pub action: ElicitationAction,
pub content: Option<Value>,
pub meta: Option<Value>,
}
pub const OPENAI_FILE_PARAMS_META_KEY: &str = "_meta";
pub const OPENAI_FILE_PARAMS_VALUE: &str = "openai/fileParams";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidedFilePayload {
#[serde(rename = "fileId")]
file_id: String,
#[serde(rename = "fileName")]
file_name: String,
#[serde(rename = "mimeType")]
mime_type: String,
#[serde(rename = "fileSize")]
file_size: i64,
}
#[derive(Debug, Clone)]
pub struct FileUploadResult {
file_id: String,
payload: ProvidedFilePayload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileParamSchemaEntry {
property_name: String,
description: String,
required: bool,
}
pub const MCP_APP_DISCLOSURE_KEY: &str = "uiDisclosure";
pub const MCP_APP_SUPPRESSED_KEY: &str = "uiSuppressed";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpAppDisclosure {
pub uri: String,
pub mime_type: Option<String>,
pub bytes: u64,
pub blocks: usize,
pub ui: Option<McpUiResourceMeta>,
}
#[must_use]
pub fn disclose_app_resource(data: &McpResourceData) -> McpAppDisclosure {
McpAppDisclosure {
uri: data.uri.clone(),
mime_type: data.contents.first().and_then(|content| match content {
ResourceContents::TextResourceContents { mime_type, .. } => mime_type.clone(),
ResourceContents::BlobResourceContents { mime_type, .. } => mime_type.clone(),
_ => None,
}),
bytes: data
.contents
.iter()
.map(|content| match content {
ResourceContents::TextResourceContents { text, .. } => text.len(),
ResourceContents::BlobResourceContents { blob, .. } => blob.len(),
_ => 0,
})
.fold(0u64, |total, len| total.saturating_add(u64::try_from(len).unwrap_or(u64::MAX))),
blocks: data.contents.len(),
ui: data
.contents
.iter()
.find_map(|content| {
let meta = match content {
ResourceContents::TextResourceContents { meta, .. } => meta,
ResourceContents::BlobResourceContents { meta, .. } => meta,
_ => return None,
};
parse_ui_resource_meta(meta.as_ref().map(|meta| &meta.0))
})
.or_else(|| parse_ui_resource_meta(Some(&data.meta))),
}
}
#[cfg(test)]
mod tests {
use super::{Map, McpResourceData, Value, disclose_app_resource};
use rmcp::model::{MetaObject, ResourceContents};
use serde_json::json;
fn text_block(text: &str, mime_type: Option<&str>, meta: Option<Value>) -> ResourceContents {
ResourceContents::TextResourceContents {
uri: "ui://demo/app.html".to_string(),
mime_type: mime_type.map(str::to_owned),
text: text.to_string(),
meta: meta.map(|value| MetaObject(serde_json::from_value(value).expect("meta object"))),
}
}
fn resource_data(contents: Vec<ResourceContents>) -> McpResourceData {
McpResourceData {
provider: "demo".to_string(),
uri: "ui://demo/app.html".to_string(),
contents,
meta: Map::new(),
}
}
#[test]
fn disclosure_derives_mime_bytes_and_ui_posture() {
let data = resource_data(vec![text_block(
"<!doctype html>",
Some("text/html;profile=mcp-app"),
Some(json!({"ui": {"permissions": {"camera": {}}}})),
)]);
let disclosure = disclose_app_resource(&data);
assert_eq!(disclosure.uri, "ui://demo/app.html");
assert_eq!(disclosure.mime_type.as_deref(), Some("text/html;profile=mcp-app"));
assert_eq!(disclosure.bytes, u64::try_from("<!doctype html>".len()).expect("small fixture"));
assert_eq!(disclosure.blocks, 1);
assert_eq!(disclosure.ui.as_ref().expect("ui posture").permissions, ["camera".to_string()]);
let value = serde_json::to_value(&disclosure).expect("serialize disclosure");
assert_eq!(value["mimeType"], "text/html;profile=mcp-app");
assert_eq!(value["ui"]["permissions"], json!(["camera"]));
}
#[test]
fn disclosure_prefers_first_block_with_ui_and_counts_blobs() {
let data = resource_data(vec![
ResourceContents::BlobResourceContents {
uri: "ui://demo/app.html".to_string(),
mime_type: None,
blob: "QUJD".to_string(),
meta: None,
},
text_block("b", None, Some(json!({"ui": {"permissions": {"microphone": {}}}}))),
]);
let disclosure = disclose_app_resource(&data);
assert_eq!(disclosure.mime_type, None);
let expected = u64::try_from("QUJD".len() + "b".len()).expect("small fixture");
assert_eq!(disclosure.bytes, expected);
assert_eq!(disclosure.blocks, 2);
assert_eq!(disclosure.ui.as_ref().expect("ui posture").permissions, ["microphone".to_string()]);
}
#[test]
fn disclosure_without_ui_has_no_posture() {
let data = resource_data(vec![text_block("body", Some("text/plain"), None)]);
let disclosure = disclose_app_resource(&data);
assert_eq!(disclosure.ui, None);
assert_eq!(disclosure.blocks, 1);
let empty = resource_data(Vec::new());
let disclosure = disclose_app_resource(&empty);
assert_eq!(disclosure.mime_type, None);
assert_eq!(disclosure.bytes, 0);
assert_eq!(disclosure.blocks, 0);
assert_eq!(disclosure.ui, None);
}
}