Skip to main content

rustlavel_mcp/
lib.rs

1//! rustlavel-mcp: the Model Context Protocol, both directions.
2//!
3//! An application uses this crate to be an MCP **server** — handing an agent
4//! its own features as tools and resources — and to be an MCP **client**,
5//! using servers somebody else wrote. Both halves speak JSON-RPC 2.0 written
6//! from scratch on [`rustlavel_core::Json`]; there is no MCP SDK underneath.
7//!
8//! # Serving
9//!
10//! Registrations go in one place, and a transport is chosen at the edge:
11//!
12//! ```ignore
13//! fn mcp() -> Server {
14//!     Server::new("shop", env!("CARGO_PKG_VERSION"))
15//!         .instructions("Use `orders.total` before quoting a refund.")
16//!         .tool(Tool::new(
17//!             "orders.total",
18//!             "Total value of one customer's orders",
19//!             Schema::object().integer("customer_id", "Which customer"),
20//!             |args: Json| async move { Ok(Json::from(total_for(&args))) },
21//!         ))
22//! }
23//!
24//! // Over HTTP, as part of the application:
25//! App::new().plugin(Mcp::new(mcp()))
26//!
27//! // Or over stdio, for a desktop client that launches the binary:
28//! stdio::serve_stdio(Arc::new(mcp())).await?
29//! ```
30//!
31//! # Consuming
32//!
33//! ```ignore
34//! let mut client = McpClient::spawn("some-mcp-server", &[])?;
35//! client.initialize().await?;
36//! for tool in client.list_tools().await? {
37//!     println!("{}: {}", tool.name, tool.description);
38//! }
39//! ```
40//!
41//! # What the design insists on
42//!
43//! * **Schemas are built, not typed.** A tool declares its input through
44//!   [`Schema`], and the same declaration is what an agent reads *and* what
45//!   the server enforces — the two cannot drift apart.
46//! * **Arguments are untrusted.** They arrive from a language model. Every
47//!   `tools/call` is validated against the declared schema before any
48//!   application code runs.
49//! * **A tool cannot take the process down.** Handlers run inside a panic
50//!   guard; a panic becomes an `isError` result the agent can read.
51//! * **Every call is instrumented.** `tools/call` dispatches an `mcp.call`
52//!   event — tool, duration, outcome — so Telescope can show it.
53
54pub mod client;
55pub mod http;
56pub mod prompt;
57pub mod protocol;
58pub mod resource;
59pub mod rpc;
60pub mod schema;
61pub mod server;
62pub mod stdio;
63pub mod tool;
64
65pub use client::McpClient;
66pub use http::Mcp;
67pub use prompt::Prompt;
68pub use protocol::{
69    Content, Implementation, PROTOCOL_VERSION, PromptArgument, PromptInfo, PromptMessage,
70    ResourceInfo, Role, ServerInfo, ToolInfo, ToolResult,
71};
72pub use resource::Resource;
73pub use rpc::{Id, RpcError};
74pub use schema::{Field, FieldType, Schema};
75pub use server::Server;
76pub use tool::Tool;
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use rustlavel_core::Json;
82    use rustlavel_http::Plugin;
83    use std::sync::Arc;
84
85    /// The shape of a real `main.rs`, exercised end to end: one server
86    /// definition, reached over both transports at once.
87    #[tokio::test]
88    async fn one_server_definition_serves_stdio_and_http_alike() {
89        let server = Arc::new(Server::new("both", "0.1.0").tool(Tool::new(
90            "double",
91            "Double a number",
92            Schema::object().integer("n", "The number to double"),
93            |args: Json| async move {
94                Ok(Json::from(args.get("n").and_then(Json::as_i64).unwrap_or(0) * 2))
95            },
96        )));
97
98        // Over stdio, through the client half.
99        let (client_side, server_side) = tokio::io::duplex(8 * 1024);
100        let (read, write) = tokio::io::split(server_side);
101        tokio::spawn(stdio::serve(Arc::clone(&server), read, write));
102
103        let (client_read, client_write) = tokio::io::split(client_side);
104        let mut mcp = McpClient::over_pipe(client_read, client_write);
105        mcp.initialize().await.unwrap();
106        let over_stdio =
107            mcp.call_tool("double", Json::object([("n", Json::from(21))])).await.unwrap();
108
109        // Over HTTP, through the router the same application serves pages from.
110        let mut router = rustlavel_http::Router::new();
111        let config = rustlavel_core::Config::with_defaults();
112        let mut context = Some(rustlavel_core::Context::builder().config(config.clone()));
113        let mut setup = rustlavel_http::Setup {
114            router: &mut router,
115            config: &config,
116            context: &mut context,
117        };
118        Box::new(Mcp::shared(Arc::clone(&server))).register(&mut setup);
119
120        let over_http = rustlavel_http::TestClient::new(router)
121            .post_json(
122                "/mcp",
123                rpc::Request::new(
124                    1i64,
125                    "tools/call",
126                    Json::object([
127                        ("name", Json::from("double")),
128                        ("arguments", Json::object([("n", Json::from(21))])),
129                    ]),
130                )
131                .to_json(),
132            )
133            .await
134            .assert_ok()
135            .json();
136
137        assert_eq!(over_stdio.text_content(), "42");
138        assert_eq!(over_http.get("result.content.0.text").unwrap().as_str(), Some("42"));
139    }
140}