mcp-attr
by: frozenlib
A library for declaratively building Model Context Protocol servers.
📌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.
-
rustfmtFriendly: Ensures all code, including AI-generated portions, can be consistently formatted usingrustfmt, 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.
rustfmtFriendly: Uses only attribute macros that can be formatted byrustfmt.
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-262024-11-05
Transport
- stdio supported
- SSE not yet supported but transport is extensible.
Methods
| Attribute | Trait Methods | Model Context Protocol Methods |
|---|---|---|
#[prompt] | prompts_listprompts_get | prompts/listprompts/get |
#[resource] | resources_listresources_readresources_templates_list | resources/listresources/readresources/templates/list |
#[tool] | tools_listtools_call | tools/listtools/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:
| Attribute | Required Trait(s) | Return Type |
|---|---|---|
#[prompt] | FromStr | GetPromptResult |
#[resource] | FromStr | ReadResourceResult |
#[tool] | DeserializeOwned + JsonSchema | CallToolResult |
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_infoinstructionscompletion_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.
