1use rmcp::model::{ElicitationAction, PromptArgument, PromptMessage, ResourceContents};
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6
7pub use vtcode_utility_tool_specs::{
8 MCP_UI_EXTENSION_ID, MCP_UI_RESOURCE_MIME_TYPE, MCP_UI_RESOURCE_SCHEME, McpToolVisibility, McpUiResourceMeta,
9 McpUiToolMeta, parse_ui_resource_meta,
10};
11
12#[derive(Debug, Clone)]
14pub struct McpToolInfo {
15 pub name: String,
16 pub description: String,
17 pub provider: String,
18 pub input_schema: Value,
19 pub output_schema: Option<Value>,
24 pub ui: Option<McpUiToolMeta>,
27 pub visibility: McpToolVisibility,
30}
31
32#[derive(Debug, Clone)]
34pub struct McpResourceInfo {
35 pub provider: String,
37 pub uri: String,
39 pub name: String,
41 pub description: Option<String>,
43 pub mime_type: Option<String>,
45 pub size: Option<i64>,
47}
48
49#[derive(Debug, Clone)]
54pub struct McpResourceData {
55 pub provider: String,
57 pub uri: String,
59 pub contents: Vec<ResourceContents>,
61 pub meta: Map<String, Value>,
63}
64
65#[derive(Debug, Clone)]
67pub struct McpPromptInfo {
68 pub(crate) provider: String,
69 pub(crate) name: String,
70 pub(crate) description: Option<String>,
71 pub(crate) arguments: Vec<PromptArgument>,
72}
73
74#[derive(Debug, Clone)]
76pub struct McpPromptDetail {
77 pub(crate) provider: String,
78 pub(crate) name: String,
79 pub(crate) description: Option<String>,
80 pub(crate) messages: Vec<PromptMessage>,
81 pub(crate) meta: Map<String, Value>,
82}
83
84#[derive(Debug, Clone)]
86pub struct McpClientStatus {
87 pub enabled: bool,
88 pub provider_count: usize,
89 pub active_connections: usize,
90 pub configured_providers: Vec<String>,
91}
92
93#[derive(Debug, Clone)]
95pub struct McpElicitationRequest {
96 pub message: String,
97 pub requested_schema: Value,
98 pub(crate) meta: Option<Value>,
99}
100
101#[derive(Debug, Clone)]
103pub struct McpElicitationResponse {
104 pub action: ElicitationAction,
105 pub content: Option<Value>,
106 pub meta: Option<Value>,
107}
108
109pub const OPENAI_FILE_PARAMS_META_KEY: &str = "_meta";
113pub const OPENAI_FILE_PARAMS_VALUE: &str = "openai/fileParams";
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ProvidedFilePayload {
122 #[serde(rename = "fileId")]
124 file_id: String,
125 #[serde(rename = "fileName")]
127 file_name: String,
128 #[serde(rename = "mimeType")]
130 mime_type: String,
131 #[serde(rename = "fileSize")]
133 file_size: i64,
134}
135
136#[derive(Debug, Clone)]
138pub struct FileUploadResult {
139 file_id: String,
141 payload: ProvidedFilePayload,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct FileParamSchemaEntry {
153 property_name: String,
155 description: String,
157 required: bool,
159}
160
161pub const MCP_APP_DISCLOSURE_KEY: &str = "uiDisclosure";
167
168pub const MCP_APP_SUPPRESSED_KEY: &str = "uiSuppressed";
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct McpAppDisclosure {
183 pub uri: String,
185 pub mime_type: Option<String>,
187 pub bytes: u64,
189 pub blocks: usize,
191 pub ui: Option<McpUiResourceMeta>,
193}
194
195#[must_use]
200pub fn disclose_app_resource(data: &McpResourceData) -> McpAppDisclosure {
201 McpAppDisclosure {
202 uri: data.uri.clone(),
203 mime_type: data.contents.first().and_then(|content| match content {
204 ResourceContents::TextResourceContents { mime_type, .. } => mime_type.clone(),
205 ResourceContents::BlobResourceContents { mime_type, .. } => mime_type.clone(),
206 _ => None,
207 }),
208 bytes: data
209 .contents
210 .iter()
211 .map(|content| match content {
212 ResourceContents::TextResourceContents { text, .. } => text.len(),
213 ResourceContents::BlobResourceContents { blob, .. } => blob.len(),
214 _ => 0,
215 })
216 .fold(0u64, |total, len| total.saturating_add(u64::try_from(len).unwrap_or(u64::MAX))),
217 blocks: data.contents.len(),
218 ui: data
219 .contents
220 .iter()
221 .find_map(|content| {
222 let meta = match content {
223 ResourceContents::TextResourceContents { meta, .. } => meta,
224 ResourceContents::BlobResourceContents { meta, .. } => meta,
225 _ => return None,
226 };
227 parse_ui_resource_meta(meta.as_ref().map(|meta| &meta.0))
228 })
229 .or_else(|| parse_ui_resource_meta(Some(&data.meta))),
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::{Map, McpResourceData, Value, disclose_app_resource};
236 use rmcp::model::{MetaObject, ResourceContents};
237 use serde_json::json;
238
239 fn text_block(text: &str, mime_type: Option<&str>, meta: Option<Value>) -> ResourceContents {
240 ResourceContents::TextResourceContents {
241 uri: "ui://demo/app.html".to_string(),
242 mime_type: mime_type.map(str::to_owned),
243 text: text.to_string(),
244 meta: meta.map(|value| MetaObject(serde_json::from_value(value).expect("meta object"))),
245 }
246 }
247
248 fn resource_data(contents: Vec<ResourceContents>) -> McpResourceData {
249 McpResourceData {
250 provider: "demo".to_string(),
251 uri: "ui://demo/app.html".to_string(),
252 contents,
253 meta: Map::new(),
254 }
255 }
256
257 #[test]
258 fn disclosure_derives_mime_bytes_and_ui_posture() {
259 let data = resource_data(vec![text_block(
260 "<!doctype html>",
261 Some("text/html;profile=mcp-app"),
262 Some(json!({"ui": {"permissions": {"camera": {}}}})),
263 )]);
264
265 let disclosure = disclose_app_resource(&data);
266 assert_eq!(disclosure.uri, "ui://demo/app.html");
267 assert_eq!(disclosure.mime_type.as_deref(), Some("text/html;profile=mcp-app"));
268 assert_eq!(disclosure.bytes, u64::try_from("<!doctype html>".len()).expect("small fixture"));
269 assert_eq!(disclosure.blocks, 1);
270 assert_eq!(disclosure.ui.as_ref().expect("ui posture").permissions, ["camera".to_string()]);
271
272 let value = serde_json::to_value(&disclosure).expect("serialize disclosure");
273 assert_eq!(value["mimeType"], "text/html;profile=mcp-app");
274 assert_eq!(value["ui"]["permissions"], json!(["camera"]));
275 }
276
277 #[test]
278 fn disclosure_prefers_first_block_with_ui_and_counts_blobs() {
279 let data = resource_data(vec![
280 ResourceContents::BlobResourceContents {
281 uri: "ui://demo/app.html".to_string(),
282 mime_type: None,
283 blob: "QUJD".to_string(),
284 meta: None,
285 },
286 text_block("b", None, Some(json!({"ui": {"permissions": {"microphone": {}}}}))),
287 ]);
288
289 let disclosure = disclose_app_resource(&data);
290 assert_eq!(disclosure.mime_type, None);
291 let expected = u64::try_from("QUJD".len() + "b".len()).expect("small fixture");
292 assert_eq!(disclosure.bytes, expected);
293 assert_eq!(disclosure.blocks, 2);
294 assert_eq!(disclosure.ui.as_ref().expect("ui posture").permissions, ["microphone".to_string()]);
295 }
296
297 #[test]
298 fn disclosure_without_ui_has_no_posture() {
299 let data = resource_data(vec![text_block("body", Some("text/plain"), None)]);
300 let disclosure = disclose_app_resource(&data);
301 assert_eq!(disclosure.ui, None);
302 assert_eq!(disclosure.blocks, 1);
303
304 let empty = resource_data(Vec::new());
305 let disclosure = disclose_app_resource(&empty);
306 assert_eq!(disclosure.mime_type, None);
307 assert_eq!(disclosure.bytes, 0);
308 assert_eq!(disclosure.blocks, 0);
309 assert_eq!(disclosure.ui, None);
310 }
311}