Expand description
§llm-tool-mcp
MCP (Model Context Protocol) stdio server
for llm-tool registries.
Register your tools in a ToolRegistry, hand it to McpServer, and get a
fully compliant MCP server — no boilerplate.
§Quick start
use llm_tool::{llm_prompt, llm_resource, llm_tool, ToolContext, ToolError, ToolRegistry};
use llm_tool_mcp::McpServer;
/// Adds two numbers.
#[llm_tool]
fn add(
/// First operand.
a: i64,
/// Second operand.
b: i64,
) -> Result<String, ToolError> {
Ok(format!("{}", a + b))
}
/// Code review instruction template.
#[llm_prompt]
fn review_prompt(
/// Programming language.
lang: String,
) -> String {
format!("Please review this {lang} code for security bugs.")
}
/// Dynamic application config resource.
#[llm_resource(uri = "file:///config/{app}.json")]
fn get_config(app: String) -> String {
format!(r#"{{"app":"{app}","enabled":true}}"#)
}
let registry = ToolRegistry::new().with_tool(Add);
let server = McpServer::builder("my-server", "0.1.0", registry)
.with_prompt(ReviewPrompt)
.with_resource(GetConfig)
.with_context(ToolContext::new().with_conversation_id("caller-id"))
.build();
// In production: server.run_stdio().expect("server failed");
// Here we feed a request via an in-memory buffer:
let input = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":17,"b":25}}}"#;
let reader = std::io::Cursor::new(format!("{input}\n"));
let mut output = Vec::new();
server.run(reader, &mut output).unwrap();
let resp: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(resp["result"]["content"][0]["text"], "42");§Transports: Stdio vs TCP vs Unix Sockets
McpServer is builder-style and supports all common execution models
out-of-the-box. Two flavors are available:
- Blocking convenience —
run_stdio,run_tcp,run_unix, and the transport-dispatchingserve(Transport). These build a Tokio runtime internally, so a simple binary’smainneeds noasync. They block until the transport finishes. - Async first-class —
run_async,listen_tcp,listen_unix. Use these when you already have a Tokio runtime (as most real applications do) so the server shares it instead of spawning a second one.
let server = McpServer::new("my-server", "0.1.0", ToolRegistry::new());
// ── Blocking convenience (no async main required) ──
// 1. Standard MCP desktop client transport (stdio subprocess):
// server.run_stdio().expect("stdio server failed");
// 2. TCP network server (localhost only):
// server.run_tcp("127.0.0.1:3000").expect("tcp server failed");
// 3. Unix Domain Socket (local IPC):
// server.run_unix("/tmp/my-agent.sock").expect("unix server failed");
// 4. Pick a transport at runtime (e.g. from CLI flags) and serve it:
let transport = Transport::Tcp("0.0.0.0:8080".parse().unwrap());
// server.serve(transport).expect("server failed");let server = McpServer::new("my-server", "0.1.0", ToolRegistry::new());
// ── Async first-class (inside an existing Tokio runtime) ──
// server.listen_tcp("127.0.0.1:3000").await.expect("tcp bind failed");
// server.listen_unix("/tmp/my-agent.sock").await.expect("unix bind failed");§What it handles
| MCP method | Behavior |
|---|---|
initialize | Returns server info and capabilities for registered primitives |
notifications/initialized | Acknowledged silently |
tools/list | Derives schemas from ToolRegistry::definitions() |
tools/call | Dispatches via ToolRegistry::dispatch(), returns content |
prompts/list | Lists all registered prompts and their argument schemas |
prompts/get | Renders prompt messages with argument substitution |
resources/list | Lists all static resources registered on the server |
resources/templates/list | Lists all URI templates (e.g. "file:///config/{app}.json") |
resources/read | Matches URIs against resources/templates and returns content |
Tool errors are returned as MCP content with isError: true (spec-compliant),
not as JSON-RPC errors.
§Async & custom transports
If running inside an existing Tokio application or network server, use run_async:
let server = McpServer::new("s", "1", ToolRegistry::new());
// Runs over any tokio::io::AsyncBufRead + AsyncWrite streams:
// server.run_async(tokio::io::stdin(), tokio::io::stdout()).await.unwrap();For custom request/response routing (e.g. Axum HTTP POST or WebSockets), call
handle_message. It accepts a single request or a JSON-RPC batch array and
returns a structured RpcOutcome — a Single response object or a Batch
array — which you can inspect or render to the wire in a single pass with
.to_wire(). None means the input was purely a notification, so there is
nothing to send back:
let server = McpServer::new("s", "1", ToolRegistry::new());
let request = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
match server.handle_message(request).await {
// `outcome` is a `Single` object or a `Batch` array — render it directly.
Some(outcome) => {
let body = outcome.to_wire();
// ...write `body` to your HTTP/WebSocket response...
assert!(body.contains("\"result\""));
}
// Notification-only input: reply 202/204 with no body.
None => {}
}§License
Dual-licensed under Apache-2.0 OR MIT.
Modules§
- protocol
- JSON-RPC 2.0 protocol types for MCP communication.
Structs§
- Connection
- Per-connection negotiated state, owned by each transport run loop.
- McpServer
- An MCP server that serves tools from a
ToolRegistryover JSON-RPC. - McpServer
Builder - Builder for
McpServerthat registers prompts and resources up front.
Enums§
- RpcOutcome
- A dispatched JSON-RPC result, ready to be inspected or rendered.
- Transport
- A transport for the blocking
McpServer::serveentry point.
Traits§
- Registry
Factory - Builds the
ToolRegistrya given caller should see.