Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,17 @@ env:
CARGO_TERM_COLOR: always

jobs:
check_format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check formatting
run: cargo fmt --all -- --check

clippy_check:
runs-on: ubuntu-latest
needs: check_format

steps:
- uses: actions/checkout@v4
- name: Run all features
Expand Down
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## 0.2.8

### Fixed
* Fixed JSON-RPC 2.0 protocol violation: server no longer sends a response to client notifications (§4 — notifications must never be replied to)
* Fixed `notifications/cancelled`: request cancellation now actually fires for both stdio and Streamable HTTP transports
* Fixed Streamable HTTP transport silently dropping notifications without processing them

## 0.2.7

### Added
* JSON-RPC Batch Support for client and server

## Fixed
* Fixed broken Streamable HTTP server implementation
### Fixed
* Fixed broken Streamable HTTP server implementation
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ exclude = [
]

[workspace.package]
version = "0.2.7"
version = "0.2.8"
license = "MIT"
edition = "2024"
rust-version = "1.90.0"
Expand All @@ -20,7 +20,7 @@ repository = "https://github.com/RomanEmreis/neva"
documentation = "https://docs.rs/neva"

[workspace.dependencies]
neva_macros = { path = "neva_macros", version = "0.2.7" }
neva_macros = { path = "neva_macros", version = "0.2.8" }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Blazingly fast and easily configurable [Model Context Protocol (MCP)](https://mo
With simple configuration and ergonomic APIs, it provides everything you need to quickly build MCP clients and servers,
fully aligned with the latest MCP specification.

[![latest](https://img.shields.io/badge/latest-0.2.7-d8eb34)](https://crates.io/crates/neva)
[![latest](https://img.shields.io/badge/rustc-1.90+-964B00)](https://crates.io/crates/neva)
[![latest](https://img.shields.io/badge/latest-0.2.8-d8eb34)](https://crates.io/crates/neva)
[![latest](https://img.shields.io/badge/rustc-1.90+-964B00)](https://releases.rs/docs/1.90.0/)
[![License: MIT](https://img.shields.io/badge/License-MIT-624bd1.svg)](https://github.com/RomanEmreis/neva/blob/main/LICENSE)
[![CI](https://github.com/RomanEmreis/neva/actions/workflows/rust.yml/badge.svg)](https://github.com/RomanEmreis/neva/actions/workflows/rust.yml)
[![Release](https://github.com/RomanEmreis/neva/actions/workflows/release.yml/badge.svg)](https://github.com/RomanEmreis/neva/actions/workflows/release.yml)
Expand All @@ -28,7 +28,7 @@ fully aligned with the latest MCP specification.
#### Dependencies
```toml
[dependencies]
neva = { version = "0.2.7", features = ["client-full"] }
neva = { version = "0.2.8", features = ["client-full"] }
tokio = { version = "1", features = ["full"] }
```

Expand Down Expand Up @@ -66,7 +66,7 @@ async fn main() -> Result<(), Error> {
#### Dependencies
```toml
[dependencies]
neva = { version = "0.2.7", features = ["server-full"] }
neva = { version = "0.2.8", features = ["server-full"] }
tokio = { version = "1", features = ["full"] }
```
#### Code
Expand Down
45 changes: 21 additions & 24 deletions examples/client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
//! cargo run -p example-client
//! ```

use std::time::Duration;
use neva::prelude::*;
use std::time::Duration;
use tracing_subscriber::prelude::*;

#[allow(dead_code)]
Expand All @@ -21,28 +21,28 @@ async fn main() -> Result<(), Error> {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.init();

let mut client = Client::new()
.with_options(|opt| opt
.with_stdio("npx", ["-y", "@modelcontextprotocol/server-everything"])

let mut client = Client::new().with_options(|opt| {
opt.with_stdio("npx", ["-y", "@modelcontextprotocol/server-everything"])
.with_roots(|roots| roots.with_list_changed())
.with_timeout(Duration::from_secs(5))
.with_mcp_version("2025-11-25"));

.with_mcp_version("2025-11-25")
});

client.connect().await?;

// Ping command
tracing::info!("--- PING ---");
let resp = client.ping().await?;
tracing::info!("{:?}", resp);

// List tools
tracing::info!("--- LIST TOOLS ---");
let tools = client.list_tools(None).await?;
for tool in tools.tools.iter() {
tracing::info!("- {}", tool.name);
}

// Call a tool
tracing::info!("--- CALL TOOL ---");
let args = ("message", "Hello MCP!");
Expand All @@ -54,11 +54,9 @@ async fn main() -> Result<(), Error> {
let tool = tools.get("get-structured-content").unwrap();
let args = ("location", "New York");
let result = client.call_tool(&tool.name, args).await?;
let weather: Weather = tool
.validate(&result)
.and_then(|res| res.as_json())?;
let weather: Weather = tool.validate(&result).and_then(|res| res.as_json())?;
tracing::info!("{:?}", weather);

// List resources
tracing::info!("--- LIST RESOURCES ---");
let resources = client.list_resources(None).await?;
Expand All @@ -73,7 +71,7 @@ async fn main() -> Result<(), Error> {
for res in resources.resources {
tracing::info!("- {}: {:?}", res.name, res.uri);
}

// List templates
tracing::info!("--- LIST RESOURCE TEMPLATES ---");
let templates = client.list_resource_templates(None).await?;
Expand All @@ -83,27 +81,26 @@ async fn main() -> Result<(), Error> {

// Read resource
tracing::info!("--- READ RESOURCE ---");
let resource = client.read_resource("demo://resource/static/document/architecture.md").await?;
let resource = client
.read_resource("demo://resource/static/document/architecture.md")
.await?;
tracing::info!("{:?}", resource.contents);

// List prompts
tracing::info!("--- LIST PROMPTS ---");
let prompts = client.list_prompts(None).await?;
for prompt in prompts.prompts {
tracing::info!("- {}, {:?}", prompt.name, prompt.args);
}

// Get prompt
tracing::info!("--- GET PROMPT ---");
let args = [
("city", "New York"),
("state", "NY")
];
let args = [("city", "New York"), ("state", "NY")];
let prompt = client.get_prompt("args-prompt", args).await?;
tracing::info!("{:?}: {:?}", prompt.descr, prompt.messages);

// This can be uncommented to check the log notifications from MCP server
//tokio::time::sleep(Duration::from_secs(60)).await;

client.disconnect().await
}
17 changes: 8 additions & 9 deletions examples/http/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Run with:
//!
//!
//! ```no_rust
//! npx @modelcontextprotocol/inspector
//!
//!
//! cargo run -p example-http
//! ```
use neva::prelude::*;
use tracing_subscriber::{filter, reload, prelude::*};
use tracing_subscriber::{filter, prelude::*, reload};

#[tool]
async fn remote_tool(name: String, mut ctx: Context) {
Expand All @@ -22,13 +22,12 @@ async fn main() {
.with(filter)
.with(notification::fmt::layer())
.init();

App::new()
.with_options(|opt| opt
.with_http(|http| http
.bind("127.0.0.1:3000")
.with_endpoint("/mcp"))
.with_logging(handle))
.with_options(|opt| {
opt.with_http(|http| http.bind("127.0.0.1:3000").with_endpoint("/mcp"))
.with_logging(handle)
})
.run()
.await;
}
10 changes: 4 additions & 6 deletions examples/large_resources_server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Run with:
//!
//! ```no_rust
//! npx @modelcontextprotocol/inspector
//! npx @modelcontextprotocol/inspector
//!
//! cargo run -p large_resources_server
//! ```
Expand All @@ -12,16 +12,14 @@ use neva::prelude::*;
async fn resource_meta(name: String) -> ResourceContents {
let uri: Uri = format!("file://{name}").into();
let res = get_res_info(uri.clone(), name.clone());

ResourceContents::new(uri)
.with_title(name)
.with_json(res)

ResourceContents::new(uri).with_title(name).with_json(res)
}

#[resource(uri = "file://{name}")]
async fn resource_data(uri: Uri, name: String) -> ResourceContents {
// get resource from somewhere

ResourceContents::new(uri.clone())
.with_title(name.clone())
.with_blob("large file")
Expand Down
20 changes: 10 additions & 10 deletions examples/logging/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! ```

use neva::prelude::*;
use tracing_subscriber::{filter, reload, prelude::*};
use tracing_subscriber::{filter, prelude::*, reload};

#[tool]
async fn trace_tool() {
Expand All @@ -18,19 +18,19 @@ async fn trace_tool() {
async fn main() {
// Configure logging filter
let (filter, handle) = reload::Layer::new(filter::LevelFilter::DEBUG);

// Configure logging
tracing_subscriber::registry()
.with(filter) // Specify the default logging level
.with(tracing_subscriber::fmt::layer()
.event_format(notification::NotificationFormatter)) // Specify the MCP notification formatter
.with(filter) // Specify the default logging level
.with(tracing_subscriber::fmt::layer().event_format(notification::NotificationFormatter)) // Specify the MCP notification formatter
.init();

App::new()
.with_options(|opt| opt
.with_stdio()
.with_mcp_version("2024-11-05")
.with_logging(handle))
.with_options(|opt| {
opt.with_stdio()
.with_mcp_version("2024-11-05")
.with_logging(handle)
})
.run()
.await;
}
30 changes: 12 additions & 18 deletions examples/middlewares/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! ```

use neva::prelude::*;
use tracing_subscriber::{prelude::*, filter, reload};
use tracing_subscriber::{filter, prelude::*, reload};

#[tool(middleware = [specific_middleware])]
async fn greeter(name: String) -> String {
Expand All @@ -19,20 +19,17 @@ async fn hello_world() -> &'static str {

#[resource(uri = "res://{name}")]
async fn resource(name: String) -> ResourceContents {
ResourceContents::new(name)
.with_text("Hello, world!")
ResourceContents::new(name).with_text("Hello, world!")
}

#[prompt(middleware = [specific_middleware])]
async fn prompt(topic: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Sample prompt of {topic}"))
PromptMessage::user().with(format!("Sample prompt of {topic}"))
}

#[prompt]
async fn another_prompt(topic: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Another sample prompt of {topic}"))
PromptMessage::user().with(format!("Another sample prompt of {topic}"))
}

#[handler(command = "ping", middleware = [specific_middleware])]
Expand All @@ -43,9 +40,9 @@ async fn ping_handler() {
async fn logging_middleware(ctx: MwContext, next: Next) -> Response {
let id = ctx.id();
tracing::info!("Request start: {id:?}");

let resp = next(ctx).await;

tracing::info!("Request end: {id:?}");
resp
}
Expand All @@ -55,7 +52,7 @@ async fn global_tool_middleware(ctx: MwContext, next: Next) -> Response {
next(ctx).await
}

// Wraps all requests for the "greeter" tool, "prompt" prompt and ping handler
// Wraps all requests for the "greeter" tool, "prompt" prompt and ping handler
async fn specific_middleware(ctx: MwContext, next: Next) -> Response {
tracing::info!("Hello from specific middleware");
next(ctx).await
Expand All @@ -65,16 +62,13 @@ async fn specific_middleware(ctx: MwContext, next: Next) -> Response {
async fn main() {
let (filter, handle) = reload::Layer::new(filter::LevelFilter::DEBUG);
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer()
.event_format(notification::NotificationFormatter))
.with(filter)
.with(tracing_subscriber::fmt::layer().event_format(notification::NotificationFormatter))
.init();

App::new()
.with_options(|opt| opt
.with_stdio()
.with_logging(handle))
.wrap(logging_middleware) // Wraps all requests that pass through the server
.with_options(|opt| opt.with_stdio().with_logging(handle))
.wrap(logging_middleware) // Wraps all requests that pass through the server
.wrap_tools(global_tool_middleware) // Wraps all tools/call requests that pass through the server
.run()
.await;
Expand Down
Loading
Loading