Skip to main content

rskit_mcp/
resources.rs

1//! Static MCP resource registration, template matching, and dispatch helpers.
2
3use std::{future::Future, pin::Pin, sync::Arc};
4
5use rmcp::model::{ReadResourceRequestParams, ReadResourceResult, Resource, ResourceTemplate};
6
7type ResourceFuture =
8    Pin<Box<dyn Future<Output = Result<ReadResourceResult, rmcp::ErrorData>> + Send>>;
9
10/// Static MCP resource registration.
11pub struct ResourceEntry {
12    /// Resource metadata exposed to clients.
13    pub resource: Resource,
14    pub(crate) handler: Arc<dyn Fn(ReadResourceRequestParams) -> ResourceFuture + Send + Sync>,
15}
16
17impl ResourceEntry {
18    /// Construct a resource entry from resource metadata and an async handler.
19    pub fn new<F, Fut>(resource: Resource, handler: F) -> Self
20    where
21        F: Fn(ReadResourceRequestParams) -> Fut + Send + Sync + 'static,
22        Fut: Future<Output = Result<ReadResourceResult, rmcp::ErrorData>> + Send + 'static,
23    {
24        Self {
25            resource,
26            handler: Arc::new(move |request| Box::pin(handler(request))),
27        }
28    }
29}
30
31impl Clone for ResourceEntry {
32    fn clone(&self) -> Self {
33        Self {
34            resource: self.resource.clone(),
35            handler: Arc::clone(&self.handler),
36        }
37    }
38}
39
40/// Static MCP resource-template registration.
41pub struct ResourceTemplateEntry {
42    /// Resource-template metadata exposed to clients.
43    pub resource_template: ResourceTemplate,
44    pub(crate) handler: Arc<dyn Fn(ReadResourceRequestParams) -> ResourceFuture + Send + Sync>,
45}
46
47impl ResourceTemplateEntry {
48    /// Construct a resource-template entry from metadata and an async handler.
49    pub fn new<F, Fut>(resource_template: ResourceTemplate, handler: F) -> Self
50    where
51        F: Fn(ReadResourceRequestParams) -> Fut + Send + Sync + 'static,
52        Fut: Future<Output = Result<ReadResourceResult, rmcp::ErrorData>> + Send + 'static,
53    {
54        Self {
55            resource_template,
56            handler: Arc::new(move |request| Box::pin(handler(request))),
57        }
58    }
59}
60
61impl Clone for ResourceTemplateEntry {
62    fn clone(&self) -> Self {
63        Self {
64            resource_template: self.resource_template.clone(),
65            handler: Arc::clone(&self.handler),
66        }
67    }
68}
69
70pub(crate) fn resource_uri(resource: &Resource) -> Option<String> {
71    serde_json::to_value(resource).ok().and_then(|value| {
72        value
73            .get("uri")
74            .and_then(|uri| uri.as_str())
75            .map(str::to_string)
76    })
77}
78
79pub(crate) fn resource_template_uri(resource_template: &ResourceTemplate) -> Option<String> {
80    serde_json::to_value(resource_template)
81        .ok()
82        .and_then(|value| {
83            value
84                .get("uriTemplate")
85                .and_then(|uri| uri.as_str())
86                .map(str::to_string)
87        })
88}
89
90pub(crate) fn resource_template_matches(template: &str, uri: &str) -> bool {
91    let literals = template_literals(template);
92    if literals.is_empty() {
93        return template == uri;
94    }
95    let Some(first) = literals.first() else {
96        return false;
97    };
98    if !uri.starts_with(first) {
99        return false;
100    }
101    let mut index = first.len();
102    for literal in literals.iter().skip(1) {
103        if literal.is_empty() {
104            continue;
105        }
106        let Some(found) = uri[index..].find(literal) else {
107            return false;
108        };
109        index += found + literal.len();
110    }
111    if !template.ends_with('}')
112        && let Some(last) = literals.last()
113        && !last.is_empty()
114    {
115        return uri.ends_with(last);
116    }
117    true
118}
119
120fn template_literals(template: &str) -> Vec<String> {
121    let mut literals = Vec::new();
122    let mut current = String::new();
123    let mut depth = 0;
124    for ch in template.chars() {
125        match ch {
126            '{' if depth == 0 => {
127                literals.push(std::mem::take(&mut current));
128                depth += 1;
129            }
130            '{' => depth += 1,
131            '}' if depth > 0 => depth -= 1,
132            _ if depth == 0 => current.push(ch),
133            _ => {}
134        }
135    }
136    literals.push(current);
137    literals
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn resource_templates_match_literals_in_order() {
146        assert!(resource_template_matches(
147            "file:///{workspace}/src/{path}",
148            "file:///rskit/src/lib.rs"
149        ));
150        assert!(resource_template_matches("urn:item/{id}", "urn:item/123"));
151        assert!(!resource_template_matches(
152            "file:///{workspace}/src/{path}",
153            "file:///rskit/tests/lib.rs"
154        ));
155        assert!(!resource_template_matches(
156            "urn:item/{id}/tail",
157            "urn:item/123/other"
158        ));
159        assert!(resource_template_matches("literal", "literal"));
160        assert!(!resource_template_matches("literal", "literal/extra"));
161    }
162
163    #[test]
164    fn nested_template_braces_are_ignored_as_placeholders() {
165        assert_eq!(
166            template_literals("prefix/{outer{inner}}/suffix"),
167            vec!["prefix/".to_string(), "/suffix".to_string()]
168        );
169    }
170}