use serde::{Deserialize, Serialize};
use serde_json::Value;
use synwire_core::mcp::traits::McpTransport;
use synwire_core::tools::BinaryResult;
use crate::error::McpAdapterError;
use crate::pagination::PaginationCursor;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpResourceDescriptor {
pub uri: String,
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone)]
pub struct McpResourceBlob {
pub uri: String,
pub text: Option<String>,
pub binary: Option<BinaryResult>,
pub mime_type: Option<String>,
}
pub async fn get_resources(
transport: &dyn McpTransport,
) -> Result<Vec<McpResourceDescriptor>, McpAdapterError> {
let mut all_resources: Vec<McpResourceDescriptor> = Vec::new();
let mut cursor = PaginationCursor::new();
loop {
let params = cursor.current().map_or_else(
|| serde_json::json!({}),
|c| serde_json::json!({ "cursor": c }),
);
let response = transport
.call_tool("resources/list", params)
.await
.map_err(|e| McpAdapterError::Transport {
message: format!("resources/list failed: {e}"),
})?;
let resources =
response["resources"]
.as_array()
.ok_or_else(|| McpAdapterError::Transport {
message: "resources/list response missing 'resources' array".into(),
})?;
for raw in resources {
match serde_json::from_value::<McpResourceDescriptor>(raw.clone()) {
Ok(r) => {
if !r.uri.contains('{') {
all_resources.push(r);
}
}
Err(e) => {
tracing::warn!(error = %e, "Skipping malformed resource descriptor");
}
}
}
let next_cursor = response["nextCursor"].as_str().map(str::to_owned);
if !cursor.advance(next_cursor) {
break;
}
}
Ok(all_resources)
}
pub async fn get_mcp_resource(
transport: &dyn McpTransport,
uri: &str,
) -> Result<McpResourceBlob, McpAdapterError> {
let params = serde_json::json!({ "uri": uri });
let response = transport
.call_tool("resources/read", params)
.await
.map_err(|e| McpAdapterError::Transport {
message: format!("resources/read failed for '{uri}': {e}"),
})?;
convert_mcp_resource_to_blob(&response, uri)
}
pub fn convert_mcp_resource_to_blob(
response: &Value,
uri: &str,
) -> Result<McpResourceBlob, McpAdapterError> {
let contents = response["contents"]
.as_array()
.ok_or_else(|| McpAdapterError::Transport {
message: format!("resources/read response missing 'contents' for '{uri}'"),
})?;
let first = contents.first().ok_or_else(|| McpAdapterError::Transport {
message: format!("resources/read response empty contents for '{uri}'"),
})?;
let text = first["text"].as_str().map(str::to_owned);
let blob_b64 = first["blob"].as_str();
let mime_type = first["mimeType"].as_str().map(str::to_owned);
let binary = blob_b64.map(|b| {
let bytes = base64_decode_simple(b);
BinaryResult {
bytes,
mime_type: mime_type
.clone()
.unwrap_or_else(|| "application/octet-stream".into()),
}
});
Ok(McpResourceBlob {
uri: uri.to_owned(),
text,
binary,
mime_type,
})
}
pub async fn load_mcp_resources(
transport: &dyn McpTransport,
) -> Result<Vec<McpResourceBlob>, McpAdapterError> {
let descriptors = get_resources(transport).await?;
let mut blobs = Vec::with_capacity(descriptors.len());
for desc in descriptors {
match get_mcp_resource(transport, &desc.uri).await {
Ok(blob) => blobs.push(blob),
Err(e) => {
tracing::warn!(uri = %desc.uri, error = %e, "Failed to load MCP resource, skipping");
}
}
}
Ok(blobs)
}
fn base64_decode_simple(s: &str) -> Vec<u8> {
let alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut lookup = [255u8; 256];
for (i, &c) in alphabet.iter().enumerate() {
lookup[usize::from(c)] = u8::try_from(i).unwrap_or(0);
}
let bytes: Vec<u8> = s
.bytes()
.filter(|b| *b != b'=')
.filter_map(|b| {
let v = lookup[usize::from(b)];
if v == 255 { None } else { Some(v) }
})
.collect();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4);
let mut i = 0;
while i + 3 < bytes.len() {
out.push((bytes[i] << 2) | (bytes[i + 1] >> 4));
out.push((bytes[i + 1] << 4) | (bytes[i + 2] >> 2));
out.push((bytes[i + 2] << 6) | bytes[i + 3]);
i += 4;
}
if i + 2 < bytes.len() {
out.push((bytes[i] << 2) | (bytes[i + 1] >> 4));
out.push((bytes[i + 1] << 4) | (bytes[i + 2] >> 2));
} else if i + 1 < bytes.len() {
out.push((bytes[i] << 2) | (bytes[i + 1] >> 4));
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn convert_text_resource() {
let response = serde_json::json!({
"contents": [{
"uri": "file:///README.md",
"text": "# Hello",
"mimeType": "text/markdown"
}]
});
let blob = convert_mcp_resource_to_blob(&response, "file:///README.md").unwrap();
assert_eq!(blob.text, Some("# Hello".into()));
assert!(blob.binary.is_none());
assert_eq!(blob.mime_type, Some("text/markdown".into()));
}
#[test]
fn dynamic_uri_detection() {
let dynamic = McpResourceDescriptor {
uri: "file:///{path}".into(),
name: "dynamic".into(),
description: None,
mime_type: None,
};
assert!(dynamic.uri.contains('{'));
}
}