rustlavel-mcp: the Model Context Protocol, both directions.
An application uses this crate to be an MCP server — handing an agent
its own features as tools and resources — and to be an MCP client,
using servers somebody else wrote. Both halves speak JSON-RPC 2.0 written
from scratch on [rustlavel_core::Json]; there is no MCP SDK underneath.
Serving
Registrations go in one place, and a transport is chosen at the edge:
fn mcp() -> Server {
Server::new("shop", env!("CARGO_PKG_VERSION"))
.instructions("Use `orders.total` before quoting a refund.")
.tool(Tool::new(
"orders.total",
"Total value of one customer's orders",
Schema::object().integer("customer_id", "Which customer"),
|args: Json| async move { Ok(Json::from(total_for(&args))) },
))
}
// Over HTTP, as part of the application:
App::new().plugin(Mcp::new(mcp()))
// Or over stdio, for a desktop client that launches the binary:
stdio::serve_stdio(Arc::new(mcp())).await?
Consuming
let mut client = McpClient::spawn("some-mcp-server", &[])?;
client.initialize().await?;
for tool in client.list_tools().await? {
println!("{}: {}", tool.name, tool.description);
}
What the design insists on
- Schemas are built, not typed. A tool declares its input through
[
Schema], and the same declaration is what an agent reads and what the server enforces — the two cannot drift apart. - Arguments are untrusted. They arrive from a language model. Every
tools/callis validated against the declared schema before any application code runs. - A tool cannot take the process down. Handlers run inside a panic
guard; a panic becomes an
isErrorresult the agent can read. - Every call is instrumented.
tools/calldispatches anmcp.callevent — tool, duration, outcome — so Telescope can show it.