A library for declaratively building Model Context Protocol servers.
mcp-attr is a crate designed to make it easy for both humans and AI to create Model Context Protocol servers. To achieve this goal, it has the following features:
- Declarative Description:
- Use attributes like
#[mcp_server]
to describe MCP servers with minimal code - Fewer lines of code make it easier for humans to understand and consume less context window for AI
- Use attributes like
- DRY (Don't Repeat Yourself) Principle:
- Declarative description ensures code follows the DRY principle
- Prevents AI from writing inconsistent code
- Leveraging the Type System:
- Expressing information sent to MCP clients through types reduces source code volume and improves readability
- Type errors help AI with coding
rustfmt
Friendly:- Only uses attribute macros that can be formatted by
rustfmt
- Ensures AI-generated code can be reliably formatted
- Only uses attribute macros that can be formatted by
Add the following to your Cargo.toml
:
[dependencies]
mcp-attr = "0.0.6"
tokio = "1.43.0"
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 {
/// Server state
count: u32,
}
#[mcp_server]
impl McpServer for ExampleServer {
/// Description sent to MCP client
#[prompt]
async fn example_prompt(&self) -> Result<&str> {
Ok("Hello!")
}
#[resource("my_app://files/{name}.txt")]
async fn read_file(&self, name: String) -> Result<String> {
Ok(format!("Content of {name}.txt"))
}
#[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))
}
}
2024-11-05
- stdio
SSE is not yet supported. However, transport is extensible, so custom transports can be implemented.
Attribute | McpServer 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 |
MCP servers created with this crate run on the tokio async runtime.
Start the server by launching the async runtime with #[tokio::main]
and passing a value implementing the McpServer
trait to the serve_stdio
function,
which starts a server using standard input/output as transport.
While you can implement the McpServer
trait manually, you can implement it more efficiently in a declarative way by using the #[mcp_server]
attribute.
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 {
#[prompt]
async fn hello(&self) -> Result<&str> {
Ok("Hello, world!")
}
}
Most of the functions implementing MCP methods are asynchronous and can be executed concurrently.
How an MCP server receives data from an MCP client is expressed through function argument definitions.
For example, in the following example, the add
tool indicates that it receives integers named lhs
and rhs
.
This information is sent from the MCP server to the MCP client, and the MCP client sends appropriate data to the server.
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))
}
}
The types that can be used for arguments vary by method, and must implement the following traits:
Attribute | Trait for argument types | Return type |
---|---|---|
#[prompt] |
FromStr |
GetPromptResult |
#[resource] |
FromStr |
ReadResourceResult |
#[tool] |
DeserializeOwned + JsonSchema |
CallToolResult |
Arguments can also use Option<T>
, in which case they are communicated to the MCP client as optional arguments.
Return values must be types that can be converted to the type shown in the Return type
column above, wrapped in Result
.
For example, since CallToolResult
implements From<String>
, you can use Result<String>
as the return value as shown in the example above.
For an MCP client to call MCP server methods, the AI needs to understand the meaning of the methods and arguments.
Adding documentation comments to methods and arguments sends this information to the MCP client, allowing the AI to understand their meaning.
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}"))
}
}
Since values implementing McpServer
are shared among multiple concurrently executing methods, only &self
is available. &mut self
cannot be used.
To maintain state, you need to use thread-safe types with interior mutability like Mutex
.
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))
}
}
mcp_attr uses Result
, Rust's standard error handling method.
The types mcp_attr::Error
and mcp_attr::Result
(an alias for std::result::Result<T, mcp_attr::Error>
) are provided for error handling.
mcp_attr::Error
is similar to anyhow::Error
, capable of storing any error type implementing std::error::Error + Sync + Send + 'static
, and implements conversion from other error types.
Therefore, in functions returning mcp_attr::Result
, you can use the ?
operator for error handling with expressions of type Result<T, impl std::error::Error + Sync + Send + 'static>
.
However, it differs from anyhow::Error
in the following ways:
- Can store JSON-RPC errors used in MCP
- Has functionality to distinguish whether error messages are public information to be sent to the MCP Client or private information not to be sent
- (However, in debug builds, all information is sent to the MCP Client)
The macros bail!
and bail_public!
are provided for error handling, similar to anyhow::bail!
.
bail!
takes a format string and arguments and raises an error treated as private information.bail_public!
takes an error code, format string, and arguments and raises an error treated as public information.
Additionally, conversions from other error types are treated as private information.
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}"))
}
}
MCP servers can call client features (such as roots/list
) using RequestContext
.
To use RequestContext
in methods implemented using attributes, add a &RequestContext
type variable to the method arguments.
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))
}
}
#[prompt("name")]
async fn func_name(&self) -> Result<GetPromptResult> { }
- "name" (optional): Prompt name. If omitted, the function name is used.
Implements the following methods:
Function arguments become prompt arguments. Arguments must implement the following trait:
FromStr
: Trait for restoring values from strings
Arguments can be given names using the #[arg("name")]
attribute.
If not specified, the name used is the function argument name with leading _
removed.
Return value: Result<impl Into<GetPromptResult>>
use mcp_attr::Result;
use mcp_attr::server::{mcp_server, McpServer};
struct ExampleServer;
#[mcp_server]
impl McpServer for ExampleServer {
/// Function description (for AI)
#[prompt]
async fn hello(&self) -> Result<&str> {
Ok("Hello, world!")
}
#[prompt]
async fn echo(&self,
/// Argument description (for AI)
a: String,
/// Argument description (for AI)
#[arg("x")]
b: String,
) -> Result<String> {
Ok(format!("Hello, {a} {b}!"))
}
}
#[resource("url_template", name = "name", mime_type = "mime_type")]
async fn func_name(&self) -> Result<ReadResourceResult> { }
- "url_template" (optional): URI Template (RFC 6570) indicating the URL of resources this method handles. If omitted, handles all URLs.
- "name" (optional): Resource name. If omitted, the function name is used.
- "mime_type" (optional): MIME type of the resource.
Implements the following methods:
resources_list
(can be manually implemented)resources_read
resources_templates_list
Function arguments become URI Template variables. Arguments must implement the following trait:
FromStr
: Trait for restoring values from strings
URI Templates are specified in RFC 6570 Level2. The following variables can be used in URI Templates:
{var}
{+var}
{#var}
Return value: Result<impl Into<ReadResourceResult>>
use mcp_attr::Result;
use mcp_attr::server::{mcp_server, McpServer};
struct ExampleServer;
#[mcp_server]
impl McpServer for ExampleServer {
/// Function description (for AI)
#[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"))
}
}
The automatically implemented resources_list
returns a list of URLs without variables specified in the #[resource]
attribute.
If you need to return other URLs, you must manually implement resources_list
.
If resources_list
is manually implemented, it is not automatically implemented.
#[tool("name")]
async fn func_name(&self) -> Result<CallToolResult> { }
- "name" (optional): Tool name. If omitted, the function name is used.
Implements the following methods:
Function arguments become tool arguments. Arguments must implement all of the following traits:
DeserializeOwned
: Trait for restoring values from JSONJsonSchema
: Trait for generating JSON Schema (JSON Schema is sent to MCP Client so AI can understand argument structure)
Arguments can be given names using the #[arg("name")]
attribute.
If not specified, the name used is the function argument name with leading _
removed.
Return value: Result<impl Into<CallToolResult>>
use mcp_attr::Result;
use mcp_attr::server::{mcp_server, McpServer};
struct ExampleServer;
#[mcp_server]
impl McpServer for ExampleServer {
/// Function description (for AI)
#[tool]
async fn echo(&self,
/// Argument description (for AI)
a: String,
/// Argument description (for AI)
#[arg("x")]
b: String,
) -> Result<String> {
Ok(format!("Hello, {a} {b}!"))
}
}
You can also directly implement McpServer
methods without using attributes.
Additionally, the following methods do not support implementation through attributes and must be implemented manually:
The following method can be overridden with manual implementation over the attribute-based implementation:
With the advent of AI Coding Agents, testing has become even more important. AI can hardly write correct code without tests, but with tests, it can write correct code through repeated testing and fixes.
mcp_attr includes McpClient
for testing, which connects to MCP servers within the process.
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(())
}
This project is dual licensed under Apache-2.0/MIT. See the two LICENSE-* files for details.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.