MCP HubMCP Hub
frozenlib

mcp-attr

by: frozenlib

A library for declaratively building Model Context Protocol servers.

19created 04/03/2025
Visit
declarative
protocol

📌Overview

Purpose: To provide a library that simplifies the creation of Model Context Protocol (MCP) servers using a declarative approach.

Overview: The mcp-attr crate facilitates the development of MCP servers by allowing developers to define server functionalities through straightforward attribute macros. This reduces code repetition and enhances clarity, making it more accessible for both developers and AI applications.

Key Features:

  • Declarative Description: Employs attributes such as #[mcp_server] to outline MCP server behaviors with minimized code, improving readability and AI context usage.

  • DRY (Don't Repeat Yourself) Principle: Promotes code reusability and eliminates inconsistencies by reducing redundancy in server method implementations.

  • Leveraging the Type System: Utilizes Rust's type system to define interactions with MCP clients, which streamlines code and assists in error handling during development.

  • rustfmt Friendly: Ensures all code, including AI-generated portions, can be consistently formatted using rustfmt, facilitating maintainable code standards.


mcp-attr

A library for declaratively building Model Context Protocol servers.

Features

mcp-attr is a crate designed to make it easy for both humans and AI to create Model Context Protocol servers.

  • Declarative Description: Use attributes like #[mcp_server] to describe MCP servers with minimal code.
  • DRY Principle: Ensures code follows the DRY principle and prevents AI from writing inconsistent code.
  • Leveraging the Type System: Uses types to express information sent to MCP clients, improving readability and reducing code volume.
  • rustfmt Friendly: Uses only attribute macros that can be formatted by rustfmt.

Quick Start

Installation

Add to your Cargo.toml:

[dependencies]
mcp-attr = "0.0.6"
tokio = "1.43.0"

Example

use std::sync::Mutex;

use mcp_attr::server::{mcp_server, McpServer, serve_stdio};
use mcp_attr::Result;

#[tokio::main]
async fn main() -> Result<()> {
    serve_stdio(ExampleServer(Mutex::new(ServerData { count: 0 }))).await?;
    Ok(())
}

struct ExampleServer(Mutex<ServerData>);

struct ServerData {
  count: u32,
}

#[mcp_server]
impl McpServer for ExampleServer {
    #[tool]
    async fn add_count(&self, message: String) -> Result<String> {
        let mut state = self.0.lock().unwrap();
        state.count += 1;
        Ok(format!("Echo: {message} {}", state.count))
    }

    #[resource("my_app://files/{name}.txt")]
    async fn read_file(&self, name: String) -> Result<String> {
        Ok(format!("Content of {name}.txt"))
    }

    #[prompt]
    async fn example_prompt(&self) -> Result<&str> {
        Ok("Hello!")
    }
}

Support Status

Protocol Versions

  • 2025-03-26
  • 2024-11-05

Transport

  • stdio supported
  • SSE not yet supported but transport is extensible.

Methods

AttributeTrait MethodsModel Context Protocol Methods
#[prompt]prompts_list
prompts_get
prompts/list
prompts/get
#[resource]resources_list
resources_read
resources_templates_list
resources/list
resources/read
resources/templates/list
#[tool]tools_list
tools_call
tools/list
tools/call

Usage

Starting the Server

Run MCP servers on the tokio async runtime. Use #[tokio::main] and call serve_stdio with a value implementing McpServer.

Example:

use mcp_attr::server::{mcp_server, McpServer, serve_stdio};
use mcp_attr::Result;

#[tokio::main]
async fn main() -> Result<()> {
  serve_stdio(ExampleServer).await?;
  Ok(())
}

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  #[tool]
  async fn hello(&self) -> Result<&str> {
    Ok("Hello, world!")
  }
}

Input and Output

MCP server method arguments define how data is received from MCP clients.

Example:

use mcp_attr::server::{mcp_server, McpServer};
use mcp_attr::Result;

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  #[tool]
  async fn add(&self, lhs: u32, rhs: u32) -> Result<String> {
    Ok(format!("{}", lhs + rhs))
  }
}

Argument types must implement certain traits depending on the attribute:

AttributeRequired Trait(s)Return Type
#[prompt]FromStrGetPromptResult
#[resource]FromStrReadResourceResult
#[tool]DeserializeOwned + JsonSchemaCallToolResult

Return values must be convertible to proper result types, wrapped in Result.

Explanations for AI

Documentation comments on methods and arguments are sent to MCP clients, giving AI clients understanding of method semantics.

Example:

use mcp_attr::server::{mcp_server, McpServer};
use mcp_attr::Result;

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  /// Tool description
  #[tool]
  async fn concat(&self,
    /// Description of argument a (for AI)
    a: u32,
    /// Description of argument b (for AI)
    b: u32,
  ) -> Result<String> {
    Ok(format!("{a},{b}"))
  }
}

State Management

Only &self is available for McpServer methods due to concurrent execution. Use thread-safe interior mutability such as Mutex to maintain state.

Example:

use std::sync::Mutex;
use mcp_attr::server::{mcp_server, McpServer};
use mcp_attr::Result;

struct ExampleServer(Mutex<ServerData>);
struct ServerData {
  count: u32,
}

#[mcp_server]
impl McpServer for ExampleServer {
  #[tool]
  async fn add_count(&self) -> Result<String> {
    let mut state = self.0.lock().unwrap();
    state.count += 1;
    Ok(format!("count: {}", state.count))
  }
}

Error Handling

Uses mcp_attr::Result for error handling with mcp_attr::Error that supports storing JSON-RPC errors and distinguishing between public and private error messages.

Provided macros:

  • bail!: raises an error treated as private information.
  • bail_public!: raises an error treated as public information with an error code.

Example:

use mcp_attr::server::{mcp_server, McpServer};
use mcp_attr::{bail, bail_public, Result, ErrorCode};

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
    #[prompt]
    async fn add(&self, a: String) -> Result<String> {
        let something_wrong = false;
        if something_wrong {
            bail_public!(ErrorCode::INTERNAL_ERROR, "Error message");
        }
        if something_wrong {
            bail!("Error message");
        }
        let a = a.parse::<i32>()?;
        Ok(format!("Success {a}"))
    }
}

Calling Client Features

Use RequestContext to call client features in methods. Add a &RequestContext argument.

Example:

use mcp_attr::server::{mcp_server, McpServer, RequestContext};
use mcp_attr::Result;

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  #[prompt]
  async fn echo_roots(&self, context: &RequestContext) -> Result<String> {
    let roots = context.roots_list().await?;
    Ok(format!("{:?}", roots))
  }
}

Attribute Descriptions

#[prompt]

#[prompt("name")]
async fn func_name(&self) -> Result<GetPromptResult> { }
  • Optional "name": prompt name; defaults to function name if omitted.

Arguments must implement FromStr.

Example:

use mcp_attr::Result;
use mcp_attr::server::{mcp_server, McpServer};

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  #[prompt]
  async fn hello(&self) -> Result<&str> {
    Ok("Hello, world!")
  }

  #[prompt]
  async fn echo(&self,
    a: String,
    #[arg("x")]
    b: String,
  ) -> Result<String> {
    Ok(format!("Hello, {a} {b}!"))
  }
}

#[resource]

#[resource("url_template", name = "name", mime_type = "mime_type")]
async fn func_name(&self) -> Result<ReadResourceResult> { }
  • "url_template": URI Template of resources handled (RFC 6570).
  • "name": resource name; defaults to function name.
  • "mime_type": MIME type.

Arguments implement FromStr.

Example:

use mcp_attr::Result;
use mcp_attr::server::{mcp_server, McpServer};

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  #[resource("my_app://x/y.txt")]
  async fn file_one(&self) -> Result<String> {
    Ok(format!("one file"))
  }

  #[resource("my_app://{a}/{+b}")]
  async fn file_ab(&self, a: String, b: String) -> Result<String> {
    Ok(format!("{a} and {b}"))
  }

  #[resource]
  async fn file_any(&self, url: String) -> Result<String> {
    Ok(format!("any file"))
  }
}

Automatically implements resources_list returning URLs without variables unless manually implemented.

#[tool]

#[tool("name")]
async fn func_name(&self) -> Result<CallToolResult> { }
  • Optional "name": tool name; defaults to function name.

Arguments must implement DeserializeOwned and JsonSchema.

Example:

use mcp_attr::Result;
use mcp_attr::server::{mcp_server, McpServer};

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
  #[tool]
  async fn echo(&self,
    a: String,
    #[arg("x")]
    b: String,
  ) -> Result<String> {
    Ok(format!("Hello, {a} {b}!"))
  }
}

Manual Implementation

You can implement McpServer methods directly without attributes.

Some methods must be implemented manually:

  • server_info
  • instructions
  • completion_complete

resources_list can be overridden manually.

Testing

Testing is important with AI coding agents.

McpClient enables in-process connection to MCP servers for testing.

Example:

use mcp_attr::client::McpClient;
use mcp_attr::server::{mcp_server, McpServer};
use mcp_attr::schema::{GetPromptRequestParams, GetPromptResult};
use mcp_attr::Result;

struct ExampleServer;

#[mcp_server]
impl McpServer for ExampleServer {
    #[prompt]
    async fn hello(&self) -> Result<&str> {
        Ok("Hello, world!")
    }
}

#[tokio::test]
async fn test_hello() -> Result<()> {
    let client = McpClient::with_server(ExampleServer).await?;
    let a = client
        .prompts_get(GetPromptRequestParams::new("hello"))
        .await?;
    let e: GetPromptResult = "Hello, world!".into();
    assert_eq!(a, e);
    Ok(())
}

License

Dual licensed under Apache-2.0/MIT.

Contribution

Contributions are dual licensed under Apache-2.0 unless explicitly stated otherwise.