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.
-
rustfmt
Friendly: 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.
rustfmt
Friendly: 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-26
2024-11-05
Transport
- stdio supported
- SSE not yet supported but transport is extensible.
Methods
Attribute | Trait Methods | Model 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:
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_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.