kuri/lib.rs
1//! kuri is a framework to build [Model Context Protocol][mcp-spec] (MCP) servers, focused on
2//! developer ergonomics and clarity.
3//!
4//! # Example
5//!
6//! The "Hello World" of kuri is:
7//!
8//! ```rust
9//! use kuri::{MCPServiceBuilder, serve, tool, ServiceExt};
10//! use kuri::transport::{StdioTransport, TransportError};
11//!
12//! #[tool]
13//! async fn hello_world_tool() -> String {
14//! "Hello World".to_string()
15//! }
16//!
17//! #[tokio::main]
18//! async fn main() -> Result<(), TransportError> {
19//! let service = MCPServiceBuilder::new("Hello World".to_string())
20//! .with_tool(HelloWorldTool)
21//! .build();
22//!
23//! serve(service.into_request_service(), StdioTransport::new()).await
24//! }
25//! ```
26//!
27//! There are more [examples] in the repository.
28//!
29//! # Getting started
30//!
31//! You'll need to add these dependencies to your `Cargo.toml`:
32//!
33//! ```toml
34//! [dependencies]
35//! kuri = "0.1"
36//! tokio = { version = "1", features = ["full"] }
37//! serde = { version = "1.0", features = ["derive"] }
38//! serde_json = "1.0"
39//! schemars = "0.8"
40//! async-trait = "0.1"
41//! ```
42//!
43//! The `full` feature of `tokio` isn't necessary, but is the easiest way to get started.
44//!
45//! # Defining tools and prompts
46//!
47//! Handlers are the functions invoked when a tool or prompt is invoked. They're just normal Rust
48//! functions, and can return any type that implements [`IntoCallToolResult`]. Since handlers are
49//! just Rust functions, you can use them as normal. Testing is also straightforward; just call the
50//! function directly.
51//!
52//! # Handling notifications
53//!
54//! If you wish to handle notifications, you'll need to define your own function to handle the raw
55//! [`Notification`] and provide this function to the [`MCPServiceBuilder`] when building your service.
56//!
57//! ```rust
58//! use kuri::{MCPServiceBuilder};
59//! use kuri_mcp_protocol::jsonrpc::Notification;
60//!
61//! async fn my_notification_handler(notification: Notification) {
62//! println!("Notification received: {:?}", notification.method);
63//! }
64//!
65//! let mut service = MCPServiceBuilder::new("Notification server".to_string())
66//! .with_notification_handler(move |_, notification| {
67//! Box::pin(my_notification_handler(notification))
68//! })
69//! .build();
70//! ```
71//!
72//! ## Error handling
73//!
74//! The MCP protocol supports two types of errors: RPC errors, and logical errors. kuri tool handlers
75//! can return a [`ToolError`], which combines both types of errors (`ExecutionError` is mapped to
76//! logical errors). You can return your own error type if you prefer; just implement
77//! [`IntoCallToolResult`] for your type.
78//!
79//! # Middleware and layers
80//!
81//! Like axum, kuri does not have its own bespoke middleware system, and instead utilises the tower
82//! ecosystem of middleware. This means you can use anything from [`tower`], [`axum`], or [`tonic`]
83//! (gRPC). Middleware can be used to implement functionality like authorisation and logging. More
84//! generally, anything that needs to happen before, after, or intercepts a request to a tool,
85//! prompt, or resource, can be implemented using tower layers with kuri.
86//!
87//! We provide [an example][middleware example] of integrating tracing using a layer. Tower also
88//! provides [a guide][tower guide to writing middleware] to get started writing middleware.
89//!
90//! ## Global middleware
91//!
92//! If your middleware needs to run on all invocations, you can apply the `.layer` using tower's
93//! [`ServiceBuilder`]:
94//! ```rust
95//! use kuri::{MCPServiceBuilder, middleware::tracing::TracingLayer};
96//! use tower::ServiceBuilder;
97//! # use kuri::tool;
98//! # #[tool]
99//! # async fn hello_world_tool() -> String {
100//! # "Hello World".to_string()
101//! # }
102//!
103//! let service = MCPServiceBuilder::new("Hello World".to_string())
104//! .with_tool(HelloWorldTool)
105//! .build();
106//!
107//! let final_service = ServiceBuilder::new()
108//! // Add tracing middleware
109//! .layer(TracingLayer::new())
110//! // Route to the MCP service
111//! .service(service);
112//! ```
113//!
114//! The layers are applied in the order they're declared, before finally routing the request to the
115//! MCP service. On return, the handlers are called in reverse order. So the first declared layer
116//! will be the first to process an incoming request, and the last to process an outgoing response.
117//!
118//! ## Per-[tool/prompt/resource] middleware
119//!
120//! For now, you will need to add the code to your handler to invoke your middleware. We're still
121//! working on making this more ergonomic within kuri.
122//!
123//! ## `.into_request_service()`
124//!
125//! [`MCPService`] is a service that processes a single JSON-RPC message (represented by [`SendableMessage`]).
126//! However, a JSON-RPC request (represented by [`Request`]) may contain a batch of messages as well.
127//! [`MCPRequestService`] is a tower service that processes these JSON-RPC requests. On the transport,
128//! you'll want to serve a service that handles the JSON-RPC requests. To turn an [`MCPService`] into a
129//! [`MCPRequestService`], you can use the `.into_request_service()` method.
130//!
131//! This has a few implications for middleware. For tracing for instance, you may want this to apply
132//! at the request level. In that case, you can use `.into_request_service()` on the service before
133//! applying your tracing middleware. Other middleware may prefer to be applied at the message level,
134//! and can be applied on [`MCPService`] instead.
135//!
136//! # Sharing state with handlers
137//!
138//! Handlers can share state with each other, and persist state across invocations, through types
139//! saved within the MCPService's [`Context`]. As in the [counter example], when creating your
140//! service, provide state to the builder. You can then access the state within your handlers using
141//! by wrapping your type in [`Inject`]:
142//!
143//! ```rust
144//! use kuri::{MCPServiceBuilder, context::Inject};
145//! use serde::Deserialize;
146//! use std::sync::atomic::{AtomicI32, Ordering};
147//!
148//! #[derive(Default, Deserialize)]
149//! struct Counter(AtomicI32);
150//!
151//! let my_state = Counter::default();
152//! let service = MCPServiceBuilder::new("Hello World".to_string())
153//! .with_state(Inject::new(my_state))
154//! .build();
155//!
156//! async fn increment(counter: Inject<Counter>, quantity: u32) {
157//! counter.0.fetch_add(quantity as i32, Ordering::SeqCst);
158//! }
159//! ```
160//!
161//! You don't need to use `Inject`, but it's the easiest way to get started. If you have more
162//! specific needs, see the [`FromContext`] trait, which you may implement for your own types.
163//!
164//! # Transports
165//!
166//! Once you instantiate a [`MCPService`], you can use the [`serve`] function to start the server
167//! over some transport, as in the Hello World example above.
168//!
169//! # Logging
170//!
171//! kuri uses tokio's tracing throughout for log messages. Typically, applications might consume
172//! these messages to stdout, however when using the stdin transport to communicate with the client,
173//! we are unable to log messages to stdout, as discussed in [the MCP docs](https://modelcontextprotocol.io/docs/tools/debugging#server-side-logging)
174//!
175//! You can change the tokio_subscriber writer to any other output stream, for example file logging:
176//! ```rust
177//! use tracing_subscriber::EnvFilter;
178//!
179//! let file_appender = tracing_appender::rolling::daily(tempfile::tempdir().unwrap(), "server.log");
180//! tracing_subscriber::fmt()
181//! .with_env_filter(EnvFilter::from_default_env())
182//! .with_writer(file_appender)
183//! .with_target(false)
184//! .with_thread_ids(true)
185//! .with_file(true)
186//! .with_line_number(true)
187//! .init();
188//! ```
189//!
190//! [mcp-spec]: https://modelcontextprotocol.io/specification/2025-03-26/
191//! [examples]: https://github.com/itsaphel/kuri/tree/main/examples
192//! [`tower`]: https://github.com/tower-rs/tower
193//! [`axum`]: https://github.com/tokio-rs/axum
194//! [`tonic`]: https://github.com/hyperium/tonic
195//! [counter example]: https://github.com/itsaphel/kuri/tree/main/examples/02_stateful_counter_tool_server.rs
196//! [middleware example]: https://github.com/itsaphel/kuri/tree/main/examples/04_hyper_middleware.rs
197//! [`ServiceBuilder`]: tower::ServiceBuilder
198//! [tower guide to writing middleware]: https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md
199//! [`IntoCallToolResult`]: crate::response::IntoCallToolResult
200//! [`Notification`]: kuri_mcp_protocol::jsonrpc::Notification
201//! [`SendableMessage`]: kuri_mcp_protocol::jsonrpc::SendableMessage
202//! [`Request`]: kuri_mcp_protocol::jsonrpc::Request
203//! [`MCPService`]: crate::MCPService
204//! [`MCPRequestService`]: crate::MCPRequestService
205//! [`MCPServer`]: crate::MCPService
206//! [`Context`]: crate::context::Context
207//! [`Inject`]: crate::context::Inject
208//! [`FromContext`]: crate::context::FromContext
209
210pub mod context;
211pub mod errors;
212mod handler;
213pub mod id;
214pub mod middleware;
215pub mod response;
216mod serve;
217mod service;
218mod service_ext;
219pub mod transport;
220
221// aliases
222pub use handler::{PromptHandler, ToolHandler};
223pub use serve::serve;
224pub use service::{MCPRequestService, MCPService, MCPServiceBuilder};
225pub use service_ext::ServiceExt;
226
227// re-export certain MCP protocol types
228pub use kuri_mcp_protocol::{
229 messages::CallToolResult, prompt::PromptArgument, prompt::PromptError, resource::ResourceError,
230 tool::generate_tool_schema, tool::ToolError,
231};
232
233// re-export macros
234pub use kuri_macros::prompt;
235pub use kuri_macros::tool;