Skip to main content

vtcode_mcp/
types.rs

1//! Core MCP data types for tool, resource, and prompt information.
2
3use 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/// Information about an MCP tool exposed by a provider.
13#[derive(Debug, Clone)]
14pub struct McpToolInfo {
15    pub name: String,
16    pub description: String,
17    pub provider: String,
18    pub input_schema: Value,
19    /// Optional output schema (`outputSchema`), when the server advertises
20    /// the structure of the tool's result. Carried for discovery fidelity so
21    /// the Full detail level returns the complete definition; execution paths
22    /// treat it as advisory metadata.
23    pub output_schema: Option<Value>,
24    /// MCP Apps linkage (`_meta.ui.resourceUri`), when the server declares a
25    /// `ui://` UI resource for this tool. Advisory display metadata only.
26    pub ui: Option<McpUiToolMeta>,
27    /// Tool accessibility between model and app (`_meta.ui.visibility`).
28    /// Model-facing listings hide tools without model visibility.
29    pub visibility: McpToolVisibility,
30}
31
32/// Summary of an MCP resource exposed by a provider.
33#[derive(Debug, Clone)]
34pub struct McpResourceInfo {
35    /// Provider that exposed the resource.
36    pub provider: String,
37    /// Canonical resource URI.
38    pub uri: String,
39    /// Human-readable resource name.
40    pub name: String,
41    /// Optional resource description.
42    pub description: Option<String>,
43    /// Advertised MIME type, when the server provides one.
44    pub mime_type: Option<String>,
45    /// Advertised size in bytes, when the server provides one.
46    pub size: Option<i64>,
47}
48
49/// Resource contents fetched from an MCP provider.
50///
51/// Per-content `_meta` (including MCP Apps `_meta.ui`) is preserved on each
52/// [`ResourceContents`] entry; `meta` carries the result-envelope metadata.
53#[derive(Debug, Clone)]
54pub struct McpResourceData {
55    /// Provider the resource was read from.
56    pub provider: String,
57    /// URI that was read.
58    pub uri: String,
59    /// Content blocks returned by `resources/read`.
60    pub contents: Vec<ResourceContents>,
61    /// Envelope `_meta` from the read result.
62    pub meta: Map<String, Value>,
63}
64
65/// Summary of an MCP prompt exposed by a provider.
66#[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/// Fully rendered MCP prompt ready for use.
75#[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/// Snapshot describing the MCP client at runtime.
85#[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/// Request payload for handling elicitation prompts from MCP providers.
94#[derive(Debug, Clone)]
95pub struct McpElicitationRequest {
96    pub message: String,
97    pub requested_schema: Value,
98    pub(crate) meta: Option<Value>,
99}
100
101/// Result returned by an elicitation handler after interacting with the user.
102#[derive(Debug, Clone)]
103pub struct McpElicitationResponse {
104    pub action: ElicitationAction,
105    pub content: Option<Value>,
106    pub meta: Option<Value>,
107}
108
109// === File Upload Types (PR #15197) ===
110
111/// Metadata key used in tool input schemas to indicate file upload parameters.
112pub const OPENAI_FILE_PARAMS_META_KEY: &str = "_meta";
113pub const OPENAI_FILE_PARAMS_VALUE: &str = "openai/fileParams";
114
115/// Shape of a file payload provided by the client after upload.
116///
117/// This is the object shape that MCP tools expecting file uploads expect to receive.
118/// The client uploads local files to the backend and replaces the local file path
119/// with this structure.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ProvidedFilePayload {
122    /// The file ID returned from the upload (maps to backend file reference)
123    #[serde(rename = "fileId")]
124    file_id: String,
125    /// The original file name
126    #[serde(rename = "fileName")]
127    file_name: String,
128    /// MIME type of the uploaded file
129    #[serde(rename = "mimeType")]
130    mime_type: String,
131    /// File size in bytes
132    #[serde(rename = "fileSize")]
133    file_size: i64,
134}
135
136/// Result of uploading a local file to the backend.
137#[derive(Debug, Clone)]
138pub struct FileUploadResult {
139    /// The file ID from the backend upload
140    file_id: String,
141    /// The payload to substitute in the tool call arguments
142    payload: ProvidedFilePayload,
143}
144
145/// Masked schema entry for a file parameter in a tool's input schema.
146///
147/// When a tool declares `_meta["openai/fileParams"]` in its input schema,
148/// we mask the raw file payload object and instead instruct the model to
149/// provide an absolute local file path. The client then handles the upload
150/// and argument rewriting transparently.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct FileParamSchemaEntry {
153    /// The original property name in the tool's input schema
154    property_name: String,
155    /// Description shown to the model for this file parameter
156    description: String,
157    /// Whether this file parameter is required
158    required: bool,
159}
160
161/// Tool-result key carrying per-URI app disclosure (`{uri: McpAppDisclosure}`).
162///
163/// Attached best-effort after execution when a result references `ui://`
164/// resources; absent when nothing was fetched. Surfaces render the linkage
165/// row alone when the key (or an entry) is missing.
166pub const MCP_APP_DISCLOSURE_KEY: &str = "uiDisclosure";
167
168/// Tool-result key listing app URIs refused by the `off` apps tier (`[String]`).
169///
170/// Attached (without any resource fetch) when a result references `ui://`
171/// resources while `mcp.ui.apps` is `off`; absent otherwise. The runner maps
172/// it to a sandbox-denied event. Like [`MCP_APP_DISCLOSURE_KEY`], display-only:
173/// never part of model context.
174pub const MCP_APP_SUPPRESSED_KEY: &str = "uiSuppressed";
175
176/// Compact disclosure facts about an MCP resource for app presentation surfaces.
177///
178/// Built by [`disclose_app_resource`] from a `resources/read` result. Surfaces
179/// render from these facts instead of re-parsing content blocks.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct McpAppDisclosure {
183    /// URI that was read.
184    pub uri: String,
185    /// MIME type of the first content block, when the server provides one.
186    pub mime_type: Option<String>,
187    /// Total body bytes across content blocks (base64 length for blobs).
188    pub bytes: u64,
189    /// Number of content blocks.
190    pub blocks: usize,
191    /// Declared UI sandbox posture, when any block (or the envelope) carries `_meta.ui`.
192    pub ui: Option<McpUiResourceMeta>,
193}
194
195/// Derive disclosure facts from a resource read result.
196///
197/// The UI posture comes from the first content block carrying `_meta.ui`,
198/// falling back to the result envelope.
199#[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}