Crate kuri

Crate kuri 

Source
Expand description

kuri is a framework to build Model Context Protocol (MCP) servers, focused on developer ergonomics and clarity.

§Example

The “Hello World” of kuri is:

use kuri::{MCPServiceBuilder, serve, tool, ServiceExt};
use kuri::transport::{StdioTransport, TransportError};

#[tool]
async fn hello_world_tool() -> String {
    "Hello World".to_string()
}

#[tokio::main]
async fn main() -> Result<(), TransportError> {
    let service = MCPServiceBuilder::new("Hello World".to_string())
        .with_tool(HelloWorldTool)
        .build();

    serve(service.into_request_service(), StdioTransport::new()).await
}

There are more examples in the repository.

§Getting started

You’ll need to add these dependencies to your Cargo.toml:

[dependencies]
kuri = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
schemars = "0.8"
async-trait = "0.1"

The full feature of tokio isn’t necessary, but is the easiest way to get started.

§Defining tools and prompts

Handlers are the functions invoked when a tool or prompt is invoked. They’re just normal Rust functions, and can return any type that implements IntoCallToolResult. Since handlers are just Rust functions, you can use them as normal. Testing is also straightforward; just call the function directly.

§Handling notifications

If you wish to handle notifications, you’ll need to define your own function to handle the raw Notification and provide this function to the MCPServiceBuilder when building your service.

use kuri::{MCPServiceBuilder};
use kuri_mcp_protocol::jsonrpc::Notification;

async fn my_notification_handler(notification: Notification) {
    println!("Notification received: {:?}", notification.method);
}

let mut service = MCPServiceBuilder::new("Notification server".to_string())
    .with_notification_handler(move |_, notification| {
        Box::pin(my_notification_handler(notification))
    })
    .build();

§Error handling

The MCP protocol supports two types of errors: RPC errors, and logical errors. kuri tool handlers can return a ToolError, which combines both types of errors (ExecutionError is mapped to logical errors). You can return your own error type if you prefer; just implement IntoCallToolResult for your type.

§Middleware and layers

Like axum, kuri does not have its own bespoke middleware system, and instead utilises the tower ecosystem of middleware. This means you can use anything from tower, axum, or tonic (gRPC). Middleware can be used to implement functionality like authorisation and logging. More generally, anything that needs to happen before, after, or intercepts a request to a tool, prompt, or resource, can be implemented using tower layers with kuri.

We provide an example of integrating tracing using a layer. Tower also provides a guide to get started writing middleware.

§Global middleware

If your middleware needs to run on all invocations, you can apply the .layer using tower’s ServiceBuilder:

use kuri::{MCPServiceBuilder, middleware::tracing::TracingLayer};
use tower::ServiceBuilder;

let service = MCPServiceBuilder::new("Hello World".to_string())
    .with_tool(HelloWorldTool)
    .build();

let final_service = ServiceBuilder::new()
    // Add tracing middleware
    .layer(TracingLayer::new())
    // Route to the MCP service
    .service(service);

The layers are applied in the order they’re declared, before finally routing the request to the MCP service. On return, the handlers are called in reverse order. So the first declared layer will be the first to process an incoming request, and the last to process an outgoing response.

§Per-[tool/prompt/resource] middleware

For now, you will need to add the code to your handler to invoke your middleware. We’re still working on making this more ergonomic within kuri.

§.into_request_service()

MCPService is a service that processes a single JSON-RPC message (represented by SendableMessage). However, a JSON-RPC request (represented by Request) may contain a batch of messages as well. MCPRequestService is a tower service that processes these JSON-RPC requests. On the transport, you’ll want to serve a service that handles the JSON-RPC requests. To turn an MCPService into a MCPRequestService, you can use the .into_request_service() method.

This has a few implications for middleware. For tracing for instance, you may want this to apply at the request level. In that case, you can use .into_request_service() on the service before applying your tracing middleware. Other middleware may prefer to be applied at the message level, and can be applied on MCPService instead.

§Sharing state with handlers

Handlers can share state with each other, and persist state across invocations, through types saved within the MCPService’s Context. As in the counter example, when creating your service, provide state to the builder. You can then access the state within your handlers using by wrapping your type in Inject:

use kuri::{MCPServiceBuilder, context::Inject};
use serde::Deserialize;
use std::sync::atomic::{AtomicI32, Ordering};

#[derive(Default, Deserialize)]
struct Counter(AtomicI32);

let my_state = Counter::default();
let service = MCPServiceBuilder::new("Hello World".to_string())
    .with_state(Inject::new(my_state))
    .build();

async fn increment(counter: Inject<Counter>, quantity: u32) {
    counter.0.fetch_add(quantity as i32, Ordering::SeqCst);
}

You don’t need to use Inject, but it’s the easiest way to get started. If you have more specific needs, see the FromContext trait, which you may implement for your own types.

§Transports

Once you instantiate a MCPService, you can use the serve function to start the server over some transport, as in the Hello World example above.

§Logging

kuri uses tokio’s tracing throughout for log messages. Typically, applications might consume these messages to stdout, however when using the stdin transport to communicate with the client, we are unable to log messages to stdout, as discussed in the MCP docs

You can change the tokio_subscriber writer to any other output stream, for example file logging:

use tracing_subscriber::EnvFilter;

let file_appender = tracing_appender::rolling::daily(tempfile::tempdir().unwrap(), "server.log");
tracing_subscriber::fmt()
    .with_env_filter(EnvFilter::from_default_env())
    .with_writer(file_appender)
    .with_target(false)
    .with_thread_ids(true)
    .with_file(true)
    .with_line_number(true)
    .init();

Modules§

context
errors
id
middleware
kuri middleware
response
transport

Structs§

CallToolResult
MCPRequestService
MCPRequestService takes a Request, which may be a batch or single message of method calls or notifications, and returns a Response, which is a batch of responses or a single (optional) response.
MCPService
A service that handles MCP requests.
MCPServiceBuilder
Build an MCPService. Tools and structs are defined when the MCPService is built. They cannot be modified after that time.
PromptArgument
Represents a prompt argument that can be passed to customize the prompt

Enums§

PromptError
ResourceError
ToolError
Errors that can be raised by a tool handler.

Traits§

PromptHandler
ServiceExt
Extension trait that adds additional methods to any Service that processes MCP messages.
ToolHandler

Functions§

generate_tool_schema
Helper function to generate JSON schema for a type
serve
Serve a MCP Service over a transport layer.

Attribute Macros§

prompt
tool