mcp-attr 0.0.7

A library for declaratively building Model Context Protocol servers.
Documentation
use pretty_assertions::assert_eq;
use tokio::test;

use mcp_attr::Result;
use mcp_attr::client::McpClient;
use mcp_attr::schema::{
    ListResourceTemplatesRequestParams, ListResourceTemplatesResult, ListResourcesRequestParams,
    ListResourcesResult, ReadResourceRequestParams, ReadResourceResult, Resource, ResourceTemplate,
};
use mcp_attr::server::{McpServer, McpServerBuilder, resource, route};

#[resource("http://localhost/a.txt")]
async fn no_arg() -> Result<&'static str> {
    Ok("abc")
}

#[resource("http://localhost/b.txt", name = "xxx")]
async fn no_arg_with_name() -> Result<&'static str> {
    Ok("def")
}

#[resource("http://localhost/b/{a}", name = "xxx2")]
async fn arg_with_name(a: String) -> Result<String> {
    Ok(format!("hello {a}"))
}

#[resource("http://localhost/c.txt", mime_type = "text/plain")]
async fn no_arg_with_mime_type() -> Result<&'static str> {
    Ok("def")
}

#[resource("http://localhost/c/{a}", mime_type = "text/plain")]
async fn arg_with_mime_type(a: String) -> Result<String> {
    Ok(format!("hello {a}"))
}

#[resource("http://localhost/se/{a}")]
async fn simple_expansion(a: String) -> Result<String> {
    Ok(format!("hello {a}"))
}

#[resource("http://localhost/re/{+a}")]
async fn reserved_expansion(a: String) -> Result<String> {
    Ok(format!("hello {a}"))
}

#[resource("http://localhost/ge/{#a}")]
async fn fragment_expansion(a: String) -> Result<String> {
    Ok(format!("hello {a}"))
}

#[resource("http://localhost/a2/{a}/{b}")]
async fn arg_2(a: String, b: String) -> Result<String> {
    Ok(format!("hello {a} {b}"))
}

#[resource("http://localhost/ao_se/{#a}")]
async fn arg_opt_simple_expansion(a: Option<String>) -> Result<String> {
    if let Some(a) = a {
        Ok(format!("hello {a}"))
    } else {
        Ok("---".to_string())
    }
}

#[resource("http://localhost/ao_re/{+a}")]
async fn arg_opt_reserved_expansion(a: Option<String>) -> Result<String> {
    if let Some(a) = a {
        Ok(format!("hello {a}"))
    } else {
        Ok("---".to_string())
    }
}

#[resource("http://localhost/ao_fe/{#a}")]
async fn arg_opt_fragment_expansion(a: Option<String>) -> Result<String> {
    if let Some(a) = a {
        Ok(format!("hello {a}"))
    } else {
        Ok("---".to_string())
    }
}

#[resource("http://localhost/au/{arg}")]
async fn arg_name_underscore(_arg: String) -> Result<String> {
    Ok("---".to_string())
}

#[resource("http://localhost/au2/{_arg}")]
async fn arg_name_underscore_2(__arg: String) -> Result<String> {
    Ok("---".to_string())
}

/// Resource Description
#[resource("http://localhost/rd")]
async fn resource_description() -> Result<&'static str> {
    Ok("resource_description")
}

/// Resource Template Description
#[resource("http://localhost/rtd/{a}")]
async fn resource_template_description(a: String) -> Result<String> {
    Ok(format!("resource_template_description {a}"))
}

#[resource]
async fn all_url(url: String) -> Result<String> {
    Ok(format!("--{url}---"))
}

fn build_server() -> Result<impl McpServer> {
    Ok(McpServerBuilder::new()
        .route(route![
            no_arg,
            no_arg_with_name,
            arg_with_name,
            no_arg_with_mime_type,
            arg_with_mime_type,
            simple_expansion,
            reserved_expansion,
            fragment_expansion,
            arg_2,
            arg_opt_simple_expansion,
            arg_opt_reserved_expansion,
            arg_opt_fragment_expansion,
            arg_name_underscore,
            arg_name_underscore_2,
            resource_description,
            resource_template_description,
            all_url
        ])
        .build())
}

async fn build_client() -> Result<McpClient> {
    Ok(McpClient::with_server(build_server()?).await?)
}

#[test]
async fn list_some() -> Result<()> {
    let client = build_client().await?;
    let a = client
        .resources_list(Some(ListResourcesRequestParams::default()))
        .await?;
    let e = resources_expected();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn list_none() -> Result<()> {
    let client = build_client().await?;
    let a = client.resources_list(None).await?;
    let e = resources_expected();
    assert_eq!(a, e);
    Ok(())
}

fn resources_expected() -> ListResourcesResult {
    vec![
        Resource::new("http://localhost/a.txt", "no_arg"),
        Resource::new("http://localhost/b.txt", "xxx"),
        Resource::new("http://localhost/c.txt", "no_arg_with_mime_type")
            .with_mime_type("text/plain"),
        Resource::new("http://localhost/rd", "resource_description")
            .with_description("Resource Description"),
    ]
    .into()
}

#[test]
async fn templates_list_some() -> Result<()> {
    let client = build_client().await?;
    let a = client
        .resources_templates_list(Some(ListResourceTemplatesRequestParams::default()))
        .await?;
    let e = templates_list_expected();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn templates_list_none() -> Result<()> {
    let client = build_client().await?;
    let a = client.resources_templates_list(None).await?;
    let e = templates_list_expected();
    assert_eq!(a, e);
    Ok(())
}

fn templates_list_expected() -> ListResourceTemplatesResult {
    vec![
        ResourceTemplate::new("http://localhost/b/{a}", "xxx2"),
        ResourceTemplate::new("http://localhost/c/{a}", "arg_with_mime_type")
            .with_mime_type("text/plain"),
        ResourceTemplate::new("http://localhost/se/{a}", "simple_expansion"),
        ResourceTemplate::new("http://localhost/re/{+a}", "reserved_expansion"),
        ResourceTemplate::new("http://localhost/ge/{#a}", "fragment_expansion"),
        ResourceTemplate::new("http://localhost/a2/{a}/{b}", "arg_2"),
        ResourceTemplate::new("http://localhost/ao_se/{#a}", "arg_opt_simple_expansion"),
        ResourceTemplate::new("http://localhost/ao_re/{+a}", "arg_opt_reserved_expansion"),
        ResourceTemplate::new("http://localhost/ao_fe/{#a}", "arg_opt_fragment_expansion"),
        ResourceTemplate::new("http://localhost/au/{arg}", "arg_name_underscore"),
        ResourceTemplate::new("http://localhost/au2/{_arg}", "arg_name_underscore_2"),
        ResourceTemplate::new("http://localhost/rtd/{a}", "resource_template_description")
            .with_description("Resource Template Description"),
    ]
    .into()
}

#[test]
async fn read_no_arg() -> Result<()> {
    let server = McpServerBuilder::new().route(route![no_arg]).build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new("http://localhost/a.txt"))
        .await?;
    let e: ReadResourceResult = "abc".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_no_arg_with_name() -> Result<()> {
    let server = McpServerBuilder::new()
        .route(route![no_arg_with_name])
        .build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new("http://localhost/b.txt"))
        .await?;
    let e: ReadResourceResult = "def".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_arg_with_name() -> Result<()> {
    let server = McpServerBuilder::new().route(route![arg_with_name]).build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new("http://localhost/b/123"))
        .await?;
    let e: ReadResourceResult = "hello 123".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_simple_expansion() -> Result<()> {
    let server = McpServerBuilder::new()
        .route(route![simple_expansion])
        .build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new("http://localhost/se/123"))
        .await?;
    let e: ReadResourceResult = "hello 123".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_simple_expansion_decode() -> Result<()> {
    let server = McpServerBuilder::new()
        .route(route![simple_expansion])
        .build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new(
            "http://localhost/se/%E3%81%82",
        ))
        .await?;
    let e: ReadResourceResult = "hello あ".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_reserved_expansion() -> Result<()> {
    let server = McpServerBuilder::new()
        .route(route![reserved_expansion])
        .build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new(
            "http://localhost/re/123/456",
        ))
        .await?;
    let e: ReadResourceResult = "hello 123/456".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_reserved_expansion_not_decode() -> Result<()> {
    let server = McpServerBuilder::new()
        .route(route![reserved_expansion])
        .build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new(
            "http://localhost/re/%E3%81%82",
        ))
        .await?;
    let e: ReadResourceResult = "hello %E3%81%82".into();
    assert_eq!(a, e);
    Ok(())
}

#[test]
async fn read_fragment_expansion() -> Result<()> {
    let server = McpServerBuilder::new()
        .route(route![fragment_expansion])
        .build();
    let client = McpClient::with_server(server).await?;
    let a = client
        .resources_read(ReadResourceRequestParams::new("http://localhost/ge/#123"))
        .await?;
    let e: ReadResourceResult = "hello 123".into();
    assert_eq!(a, e);
    Ok(())
}