Skip to main content

flymcprust_core/
resource.rs

1//! Resource definitions and contents.
2//!
3//! Resources are usually declared with the `#[resource]` macro, which
4//! implements [`Resource`] for a marker type. [`ResourceDef`] can also be built
5//! by hand.
6
7use std::borrow::Cow;
8use std::future::Future;
9use std::pin::Pin;
10use std::time::Duration;
11
12use base64::Engine as _;
13use serde::Serialize;
14
15use crate::completion::{CompleteFn, Completer};
16use crate::context::Ctx;
17use crate::spec::{self, BlobResourceContents, CacheScope, TextResourceContents};
18use crate::tool::{IntoToolError, Json, StateRequirement, ToolError};
19use crate::uri_template::UriVars;
20
21/// One item of a `resources/read` result.
22pub type ResourceContents = spec::ReadResourceResultContentsItem;
23
24/// The future returned by a resource read.
25pub type ResourceFuture =
26    Pin<Box<dyn Future<Output = Result<Vec<ResourceContents>, ToolError>> + Send + 'static>>;
27
28/// Entry point of a resource: receives the requested URI and, for templates,
29/// the variables extracted from it.
30pub type ReadFn = fn(uri: &str, vars: &UriVars, ctx: Ctx) -> Result<ResourceFuture, ToolError>;
31
32/// A resource that can be registered with
33/// [`ServerBuilder::resource`](crate::ServerBuilder::resource).
34pub trait Resource: 'static {
35    /// Builds the resource definition. Called once, at registration.
36    fn definition() -> ResourceDef;
37}
38
39#[derive(Debug, Clone)]
40pub(crate) enum ResourceKind {
41    Static(spec::Resource),
42    Template(spec::ResourceTemplate),
43}
44
45/// A registered resource or resource template.
46#[derive(Debug, Clone)]
47pub struct ResourceDef {
48    pub(crate) kind: ResourceKind,
49    pub(crate) read: ReadFn,
50    pub(crate) required_state: Vec<StateRequirement>,
51    pub(crate) ttl: Duration,
52    pub(crate) cache_scope: CacheScope,
53    pub(crate) completers: Vec<Completer>,
54    pub(crate) scopes: Vec<String>,
55    pub(crate) app: Option<crate::apps::AppUi>,
56}
57
58impl ResourceDef {
59    fn with_kind(kind: ResourceKind, read: ReadFn) -> Self {
60        Self {
61            kind,
62            read,
63            required_state: Vec::new(),
64            ttl: Duration::ZERO,
65            cache_scope: CacheScope::Private,
66            completers: Vec::new(),
67            scopes: Vec::new(),
68            app: None,
69        }
70    }
71
72    /// Makes this an MCP Apps interface: an HTML5 page served with the
73    /// `text/html;profile=mcp-app` MIME type at a `ui://` URI, sandboxed as
74    /// `ui` describes. Tools show it with [`ToolDef::ui`](crate::ToolDef::ui).
75    #[must_use]
76    pub fn app(mut self, ui: crate::apps::AppUi) -> Self {
77        if let ResourceKind::Static(resource) = &mut self.kind {
78            resource.mime_type = Some(crate::apps::MIME_TYPE.to_owned());
79            resource
80                .meta
81                .get_or_insert_with(Default::default)
82                .insert("ui".to_owned(), ui.to_meta());
83        }
84        self.app = Some(ui);
85        self
86    }
87
88    /// Whether this is an MCP Apps interface.
89    #[must_use]
90    pub fn is_app(&self) -> bool {
91        self.app.is_some()
92    }
93
94    /// Declares the OAuth scopes a caller needs (all of them). Enforced when
95    /// the HTTP transport requires OAuth; the client gets `403` with an
96    /// `insufficient_scope` challenge listing these scopes.
97    #[must_use]
98    pub fn scopes<I, S>(mut self, scopes: I) -> Self
99    where
100        I: IntoIterator<Item = S>,
101        S: Into<String>,
102    {
103        self.scopes = scopes.into_iter().map(Into::into).collect();
104        self
105    }
106
107    /// The OAuth scopes declared with [`scopes`](Self::scopes).
108    #[must_use]
109    pub fn required_scopes(&self) -> &[String] {
110        &self.scopes
111    }
112
113    /// A resource with a fixed URI (listed by `resources/list`).
114    #[must_use]
115    pub fn new(resource: spec::Resource, read: ReadFn) -> Self {
116        Self::with_kind(ResourceKind::Static(resource), read)
117    }
118
119    /// A resource template (listed by `resources/templates/list`). The
120    /// `uriTemplate` must use the supported RFC 6570 subset (see
121    /// [`UriTemplate`](crate::UriTemplate)).
122    #[must_use]
123    pub fn template(template: spec::ResourceTemplate, read: ReadFn) -> Self {
124        Self::with_kind(ResourceKind::Template(template), read)
125    }
126
127    /// Declares that the resource needs shared state of type `T`.
128    #[must_use]
129    pub fn require_state<T: Send + Sync + 'static>(mut self) -> Self {
130        self.required_state.push(StateRequirement::of::<T>());
131        self
132    }
133
134    /// Freshness hint (`ttlMs`) of read results. Default: zero (always
135    /// re-fetch).
136    #[must_use]
137    pub fn ttl(mut self, ttl: Duration) -> Self {
138        self.ttl = ttl;
139        self
140    }
141
142    /// `cacheScope` of read results. Default: `private`, since contents often
143    /// depend on the caller. Use `public` for data identical for everyone.
144    #[must_use]
145    pub fn cache_scope(mut self, scope: CacheScope) -> Self {
146        self.cache_scope = scope;
147        self
148    }
149
150    /// Attaches a completion provider to a template variable.
151    #[must_use]
152    pub fn complete(mut self, variable: impl Into<String>, complete: CompleteFn) -> Self {
153        self.completers.push(Completer {
154            argument: variable.into(),
155            complete,
156        });
157        self
158    }
159
160    /// The URI (static resources) or URI template.
161    #[must_use]
162    pub fn uri(&self) -> &str {
163        match &self.kind {
164            ResourceKind::Static(r) => &r.uri,
165            ResourceKind::Template(t) => &t.uri_template,
166        }
167    }
168
169    /// Whether this is a resource template.
170    #[must_use]
171    pub fn is_template(&self) -> bool {
172        matches!(self.kind, ResourceKind::Template(_))
173    }
174}
175
176/// Conversion of a resource handler's return value into contents.
177///
178/// | Return type | Contents |
179/// | --- | --- |
180/// | `String`, `&'static str`, `Cow<'static, str>` | Text |
181/// | `Vec<u8>` | Binary (base64 `blob`) |
182/// | `Json<T>` | JSON text (`application/json` unless another MIME type is set) |
183/// | `TextResourceContents`, `BlobResourceContents`, `ResourceContents`, `Vec<ResourceContents>` | As given |
184/// | `Result<T, E>` | `T` as above; `E` as for tools |
185///
186/// The `uri` of generated contents is the requested URI, and their MIME type
187/// is the one declared on the resource.
188pub trait IntoResourceContents {
189    /// Performs the conversion.
190    ///
191    /// # Errors
192    ///
193    /// Returns the handler's error, or an internal error if serialization
194    /// fails.
195    fn into_contents(
196        self,
197        uri: &str,
198        mime_type: Option<&str>,
199    ) -> Result<Vec<ResourceContents>, ToolError>;
200}
201
202fn text(uri: &str, mime_type: Option<&str>, text: String) -> Vec<ResourceContents> {
203    let mut contents = TextResourceContents::new(text, uri.to_owned());
204    contents.mime_type = mime_type.map(str::to_owned);
205    vec![ResourceContents::Text(contents)]
206}
207
208impl IntoResourceContents for String {
209    fn into_contents(
210        self,
211        uri: &str,
212        mime_type: Option<&str>,
213    ) -> Result<Vec<ResourceContents>, ToolError> {
214        Ok(text(uri, mime_type, self))
215    }
216}
217
218impl IntoResourceContents for &'static str {
219    fn into_contents(
220        self,
221        uri: &str,
222        mime_type: Option<&str>,
223    ) -> Result<Vec<ResourceContents>, ToolError> {
224        Ok(text(uri, mime_type, self.to_owned()))
225    }
226}
227
228impl IntoResourceContents for Cow<'static, str> {
229    fn into_contents(
230        self,
231        uri: &str,
232        mime_type: Option<&str>,
233    ) -> Result<Vec<ResourceContents>, ToolError> {
234        Ok(text(uri, mime_type, self.into_owned()))
235    }
236}
237
238impl IntoResourceContents for Vec<u8> {
239    fn into_contents(
240        self,
241        uri: &str,
242        mime_type: Option<&str>,
243    ) -> Result<Vec<ResourceContents>, ToolError> {
244        let blob = base64::engine::general_purpose::STANDARD.encode(self);
245        let mut contents = BlobResourceContents::new(blob, uri.to_owned());
246        contents.mime_type = mime_type.map(str::to_owned);
247        Ok(vec![ResourceContents::Blob(contents)])
248    }
249}
250
251impl<T: Serialize> IntoResourceContents for Json<T> {
252    fn into_contents(
253        self,
254        uri: &str,
255        mime_type: Option<&str>,
256    ) -> Result<Vec<ResourceContents>, ToolError> {
257        let json = serde_json::to_string(&self.0).map_err(ToolError::internal)?;
258        Ok(text(
259            uri,
260            Some(mime_type.unwrap_or("application/json")),
261            json,
262        ))
263    }
264}
265
266impl IntoResourceContents for TextResourceContents {
267    fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
268        Ok(vec![ResourceContents::Text(self)])
269    }
270}
271
272impl IntoResourceContents for BlobResourceContents {
273    fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
274        Ok(vec![ResourceContents::Blob(self)])
275    }
276}
277
278impl IntoResourceContents for ResourceContents {
279    fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
280        Ok(vec![self])
281    }
282}
283
284impl IntoResourceContents for Vec<ResourceContents> {
285    fn into_contents(self, _: &str, _: Option<&str>) -> Result<Vec<ResourceContents>, ToolError> {
286        Ok(self)
287    }
288}
289
290impl<T: IntoResourceContents, E: IntoToolError> IntoResourceContents for Result<T, E> {
291    fn into_contents(
292        self,
293        uri: &str,
294        mime_type: Option<&str>,
295    ) -> Result<Vec<ResourceContents>, ToolError> {
296        match self {
297            Ok(value) => value.into_contents(uri, mime_type),
298            Err(error) => Err(error.into_tool_error()),
299        }
300    }
301}
302
303/// Parses a template variable for a resource handler. A value that does not
304/// parse means no such resource exists.
305///
306/// # Errors
307///
308/// Returns [`ToolError::not_found`] if the variable is missing or invalid.
309pub fn parse_uri_var<T: std::str::FromStr>(vars: &UriVars, name: &str) -> Result<T, ToolError> {
310    vars.get(name)
311        .and_then(|v| v.parse().ok())
312        .ok_or_else(ToolError::not_found)
313}
314
315/// Like [`parse_uri_var`] for optional (query) variables.
316///
317/// # Errors
318///
319/// Returns [`ToolError::not_found`] if the variable is present but invalid.
320pub fn parse_optional_uri_var<T: std::str::FromStr>(
321    vars: &UriVars,
322    name: &str,
323) -> Result<Option<T>, ToolError> {
324    vars.get(name)
325        .map(|v| v.parse().map_err(|_| ToolError::not_found()))
326        .transpose()
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn contents_conversions() {
335        let t = "hi".into_contents("a://b", Some("text/plain")).unwrap();
336        assert_eq!(
337            serde_json::to_value(&t).unwrap(),
338            serde_json::json!([{ "uri": "a://b", "mimeType": "text/plain", "text": "hi" }])
339        );
340        let b = vec![1u8, 2, 3].into_contents("a://b", None).unwrap();
341        assert_eq!(
342            serde_json::to_value(&b).unwrap(),
343            serde_json::json!([{ "uri": "a://b", "blob": "AQID" }])
344        );
345        let j = Json(serde_json::json!({ "x": 1 }))
346            .into_contents("a://b", None)
347            .unwrap();
348        assert_eq!(
349            serde_json::to_value(&j).unwrap(),
350            serde_json::json!([{ "uri": "a://b", "mimeType": "application/json", "text": "{\"x\":1}" }])
351        );
352    }
353}