use serde_json::{json, Value};
use super::tools::McpContext;
use super::types::{codes, JsonRpcError};
pub async fn list_prompts(ctx: &McpContext) -> Result<Value, JsonRpcError> {
let skills = crate::tenancy::skills_by_codenames_pool(&ctx.pool, &ctx.agent.skills)
.await
.map_err(internal)?;
let prompts: Vec<Value> = skills
.iter()
.map(|s| json!({ "name": s.codename, "description": s.description, "arguments": [] }))
.collect();
Ok(json!({ "prompts": prompts }))
}
pub async fn get_prompt(ctx: &McpContext, params: Value) -> Result<Value, JsonRpcError> {
let name = params
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| JsonRpcError::invalid_params("prompts/get requires a string `name`"))?;
if !ctx.agent.skills.iter().any(|s| s == name) {
return Err(JsonRpcError::new(
codes::TOOL_FORBIDDEN,
format!("prompt `{name}` is not granted to this agent"),
));
}
let skill = crate::tenancy::skills_by_codenames_pool(&ctx.pool, &[name.to_owned()])
.await
.map_err(internal)?
.into_iter()
.next()
.ok_or_else(|| {
JsonRpcError::new(codes::TOOL_FORBIDDEN, format!("unknown prompt: {name}"))
})?;
Ok(json!({
"description": skill.description,
"messages": [{
"role": "user",
"content": { "type": "text", "text": skill.instructions },
}],
}))
}
pub struct McpResource {
pub uri: &'static str,
pub name: &'static str,
pub mime_type: &'static str,
pub read: fn() -> String,
}
inventory::collect!(McpResource);
fn static_resource(uri: &str) -> Option<&'static McpResource> {
inventory::iter::<McpResource>
.into_iter()
.find(|r| r.uri == uri)
}
pub async fn list_resources(ctx: &McpContext) -> Result<Value, JsonRpcError> {
let mut out: Vec<Value> = inventory::iter::<McpResource>
.into_iter()
.map(|r| json!({ "uri": r.uri, "name": r.name, "mimeType": r.mime_type }))
.collect();
let skill_resources = crate::tenancy::resources_for_skills_pool(&ctx.pool, &ctx.agent.skills)
.await
.map_err(internal)?;
for r in &skill_resources {
out.push(json!({ "uri": r.resource_uri, "mimeType": r.mime }));
}
Ok(json!({ "resources": out }))
}
pub async fn read_resource(ctx: &McpContext, params: Value) -> Result<Value, JsonRpcError> {
let uri = params
.get("uri")
.and_then(Value::as_str)
.ok_or_else(|| JsonRpcError::invalid_params("resources/read requires a string `uri`"))?;
if let Some(r) = static_resource(uri) {
return Ok(contents(uri, r.mime_type, (r.read)()));
}
let skill_resources = crate::tenancy::resources_for_skills_pool(&ctx.pool, &ctx.agent.skills)
.await
.map_err(internal)?;
let res = skill_resources
.into_iter()
.find(|r| r.resource_uri == uri)
.ok_or_else(|| {
JsonRpcError::new(
codes::TOOL_FORBIDDEN,
format!("resource `{uri}` is not available to this agent"),
)
})?;
let text = res
.data
.get("text")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| res.data.to_string());
Ok(contents(uri, &res.mime, text))
}
fn contents(uri: &str, mime: &str, text: String) -> Value {
json!({ "contents": [{ "uri": uri, "mimeType": mime, "text": text }] })
}
fn internal(e: crate::tenancy::AgentError) -> JsonRpcError {
JsonRpcError::new(codes::INTERNAL_ERROR, e.to_string())
}
#[macro_export]
macro_rules! register_mcp_resource {
($uri:expr, $name:expr, $mime:expr, $read:expr $(,)?) => {
$crate::inventory::submit! {
$crate::mcp::McpResource {
uri: $uri,
name: $name,
mime_type: $mime,
read: {
fn __rustango_mcp_resource_read() -> ::std::string::String {
($read)()
}
__rustango_mcp_resource_read
},
}
}
};
}