1 unstable release
new 0.0.1 | Mar 8, 2025 |
---|
#27 in #mcp
Used in mcp-attr
42KB
1K
SLoC
mcp-attr
A crate for creating Model Context Protocol servers using declarative descriptions with attributes and types.
Features
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
- Following the DRY (Don't Repeat Yourself) principle helps prevent inconsistent code
- Use attributes like
- 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
Quick Start
Installation
Add the following to your Cargo.toml
:
[dependencies]
mcp-attr = "0.0.1"
tokio = "1.43.0"
Basic Usage
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))
}
}
Support Status
The following protocol versions, transports, and methods are supported.
Protocol Versions
2024-11-05
Transport
- stdio
SSE is not yet supported. However, transport is extensible, so custom transports can be implemented.
Methods
Attribute | MpcServer 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
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.
Input and Output
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.
Explanations for AI
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}"))
}
}
State Management
Since values implementing MpcServer
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))
}
}
Error Handling
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}"))
}
}
Attribute Descriptions
#[prompt]
#[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::server::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]
#[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::server::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]
#[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::server::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}!"))
}
}
Manual Implementation
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:
server_info
instructions
completion_complete
The following method can have its attribute-based implementation overridden with manual implementation:
tools_list
Testing
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 correction.
mcp-attr includes a test MCP Client that 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::from_server(ExampleServer).await?;
let a = client
.prompts_get(GetPromptRequestParams::new("hello"))
.await?;
let e: GetPromptResult = "Hello, world!".into();
assert_eq!(a, e);
Ok(())
}
License
This project is dual licensed under Apache-2.0/MIT. See the two LICENSE-* files for details.
Contribution
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.
Dependencies
~2.8–4.5MB
~88K SLoC