Skip to main content

Basics

Let's build a simple MCP server with Neva and add a tool, prompt and resource handlers.

Create an app

Create a new binary-based app:

cargo new neva-mcp-server
cd neva-mcp-server

Add the following dependencies in your Cargo.toml:

[dependencies]
neva = { version = "0.2.0", features = "server-full" }
tokio = { version = "1", features = ["full"] }

Setup a tool

Let's start by adding a simple tool - a function that greets a user by name.

Create your main application in main.rs:

use neva::prelude::*;

#[tool(descr = "A say hello tool")]
async fn hello(name: String) -> String {
format!("Hello, {name}!")
}

#[tokio::main]
async fn main() {
App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("Sample MCP server")
.with_version("1.0.0"))
.run()
.await;
}

In the code above configured the MCP Server that runs on stdio transport and declared an async tool handler by using a tool attribute macro that extracts the name parameter into a String and expects another result String to be returned. The macro registers our hello tool with the specified description.

Besides the descr, you can configure your tool with:

  • title - Tool title.
  • input_schema - Schema for the tool input.
  • output_schema - Schema for the tool output.
  • annotations - Arbitrary metadata.
  • roles & permissions - Define which users can run the tool when using Streamable HTTP transport with OAuth.

Testing the MCP Server

For testing purposes you may leverage the MCP Inspector by running the following command:

npx @modelcontextprotocol/inspector cargo run

This launches the MCP Inspector UI, allowing you to explore your server’s tools, prompts, and resources interactively.

Adding a prompt handler

Next, we'll similarly add the prompt handler by using the prompt attribute macro:

#[prompt(descr = "Analyze code for potential improvements")]
async fn analyze_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Language: {lang}"))
}

Adding a resource tempate handler

Same idea as above, with the special resource attribute macro you can define a resource handler with minimal boilerplate:

#[resource(
uri = "res://{name}",
title = "Read resource",
descr = "Some details about resource",
mime = "application/octet-stream",
annotations = r#"{
"audience": ["user"],
"priority": 1.0
}"#
)]
async fn get_res(uri: Uri, name: String) -> ResourceContents {
let data = "some file contents"; // Read a resource from some source

ResourceContents::new(uri)
.with_title(name)
.with_blob(data)
}

Learn By Example

Here you may find the full example