Move whole rmcp crate to offcial rust sdk (#44)
* get rid of async-trait * move rmcp to official-rust-sdk * replace master with main * update readme, change license * fix typo * Update README.md Co-authored-by: Jefry Dewangga <jefrydco@gmail.com> * fix typos --------- Co-authored-by: = <=> Co-authored-by: Jefry Dewangga <jefrydco@gmail.com>
This commit is contained in:
parent
31493cfbcc
commit
0be6067326
10 changed files with 8676 additions and 4466 deletions
|
|
@ -1,48 +1,36 @@
|
|||
[package]
|
||||
name = "mcp-server-examples"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version = "0.1.5"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
mcp-server = { path = "../../crates/mcp-server" }
|
||||
mcp-core = { path = "../../crates/mcp-core" }
|
||||
mcp-macros = { path = "../../crates/mcp-macros" }
|
||||
tokio = { version = "1", features = ["io-util"] }
|
||||
rmcp= { path = "../../crates/rmcp", features = ["server", "transport-sse-server", "transport-io"] }
|
||||
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "io-std", "signal"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"env-filter",
|
||||
"std",
|
||||
"fmt",
|
||||
] }
|
||||
futures = "0.3"
|
||||
rand = { version = "0.9" }
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
schemars = { version = "0.8", optional = true }
|
||||
# [dev-dependencies.'cfg(target_arch="linux")'.dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
tokio-util = { version = "0.7", features = ["io", "codec"]}
|
||||
rand = { version = "0.8" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
actix-web = "4"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
tokio = { version = "1", features = ["io-util", "rt", "time", "macros"] }
|
||||
wasi = { version = "0.11.0+wasi-snapshot-preview1" }
|
||||
tokio-stream = { version = "0.1" }
|
||||
# tokio-util = { version = "0.7", features = ["io", "codec"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
[[example]]
|
||||
name = "counter-server"
|
||||
path = "src/counter_server.rs"
|
||||
name = "std_io"
|
||||
path = "src/std_io.rs"
|
||||
|
||||
[[example]]
|
||||
name = "axum"
|
||||
path = "src/axum.rs"
|
||||
|
||||
[[example]]
|
||||
name = "wasi_std_io"
|
||||
path = "src/wasi_std_io.rs"
|
||||
|
||||
[[example]]
|
||||
name = "actix_web"
|
||||
path = "src/actix_web.rs"
|
||||
path = "src/axum.rs"
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
use actix_web::web::{Bytes, Data, Payload, Query};
|
||||
use actix_web::{get, post, App, Error, HttpResponse, HttpServer, Result};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use mcp_server::{ByteTransport, Server};
|
||||
use std::collections::HashMap;
|
||||
use tokio_util::codec::FramedRead;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use actix_web::middleware::Logger;
|
||||
use mcp_server::router::RouterService;
|
||||
use std::sync::Arc;
|
||||
use tokio::{
|
||||
io::{self, AsyncWriteExt},
|
||||
sync::Mutex,
|
||||
};
|
||||
mod common;
|
||||
use common::counter;
|
||||
|
||||
type C2SWriter = Arc<Mutex<io::WriteHalf<io::SimplexStream>>>;
|
||||
type SessionId = Arc<str>;
|
||||
|
||||
const BIND_ADDRESS: &str = "127.0.0.1:8000";
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AppState {
|
||||
txs: Arc<tokio::sync::RwLock<HashMap<SessionId, C2SWriter>>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
txs: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn session_id() -> SessionId {
|
||||
let id = format!("{:016x}", rand::random::<u128>());
|
||||
Arc::from(id)
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PostEventQuery {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
#[post("/sse")]
|
||||
async fn post_event_handler(
|
||||
app_state: Data<AppState>,
|
||||
query: Query<PostEventQuery>,
|
||||
mut payload: Payload,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
const BODY_BYTES_LIMIT: usize = 1 << 22;
|
||||
let session_id = &query.session_id;
|
||||
|
||||
let write_stream = {
|
||||
let rg = app_state.txs.read().await;
|
||||
match rg.get(session_id.as_str()) {
|
||||
Some(stream) => stream.clone(),
|
||||
None => return Ok(HttpResponse::NotFound().finish()),
|
||||
}
|
||||
};
|
||||
|
||||
let mut write_stream = write_stream.lock().await;
|
||||
let mut size = 0;
|
||||
|
||||
// Process the request body in chunks
|
||||
while let Some(chunk) = payload.next().await {
|
||||
let chunk = chunk?;
|
||||
size += chunk.len();
|
||||
if size > BODY_BYTES_LIMIT {
|
||||
return Ok(HttpResponse::PayloadTooLarge().finish());
|
||||
}
|
||||
|
||||
if (write_stream.write_all(&chunk).await).is_err() {
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
}
|
||||
|
||||
if (write_stream.write_u8(b'\n').await).is_err() {
|
||||
return Ok(HttpResponse::InternalServerError().finish());
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Accepted().finish())
|
||||
}
|
||||
|
||||
#[get("/sse")]
|
||||
async fn sse_handler(app_state: Data<AppState>) -> Result<HttpResponse, Error> {
|
||||
// it's 4KB
|
||||
const BUFFER_SIZE: usize = 1 << 12;
|
||||
let session = session_id();
|
||||
tracing::info!(%session, "sse connection");
|
||||
|
||||
let (c2s_read, c2s_write) = tokio::io::simplex(BUFFER_SIZE);
|
||||
let (s2c_read, s2c_write) = tokio::io::simplex(BUFFER_SIZE);
|
||||
|
||||
app_state
|
||||
.txs
|
||||
.write()
|
||||
.await
|
||||
.insert(session.clone(), Arc::new(Mutex::new(c2s_write)));
|
||||
|
||||
{
|
||||
let session = session.clone();
|
||||
let app_state = app_state.clone();
|
||||
tokio::spawn(async move {
|
||||
let router = RouterService(counter::CounterRouter::new());
|
||||
let server = Server::new(router);
|
||||
let bytes_transport = ByteTransport::new(c2s_read, s2c_write);
|
||||
let _result = server
|
||||
.run(bytes_transport)
|
||||
.await
|
||||
.inspect_err(|e| tracing::error!(?e, "server run error"));
|
||||
tracing::info!(%session, "connection closed, removing session");
|
||||
app_state.txs.write().await.remove(&session);
|
||||
});
|
||||
}
|
||||
|
||||
// Create SSE stream with correct types
|
||||
let stream = futures::stream::once(futures::future::ready(Ok::<_, io::Error>(Bytes::from(
|
||||
format!("event: endpoint\ndata: ?sessionId={}\n\n", session),
|
||||
))))
|
||||
.chain(
|
||||
FramedRead::new(s2c_read, common::jsonrpc_frame_codec::JsonRpcFrameCodec)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
.map_ok(move |bytes| {
|
||||
let message = match std::str::from_utf8(&bytes) {
|
||||
Ok(message) => format!("event: message\ndata: {}\n\n", message),
|
||||
Err(_) => "event: error\ndata: Invalid UTF-8 data\n\n".to_string(),
|
||||
};
|
||||
Bytes::from(message)
|
||||
}),
|
||||
);
|
||||
|
||||
// Return SSE response
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/event-stream"))
|
||||
.append_header(("Cache-Control", "no-cache"))
|
||||
.append_header(("Connection", "keep-alive"))
|
||||
.streaming(stream))
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
tracing::debug!("starting server at {}", BIND_ADDRESS);
|
||||
|
||||
let app_state = Data::new(AppState::new());
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.app_data(app_state.clone())
|
||||
.service(sse_handler)
|
||||
.service(post_event_handler)
|
||||
})
|
||||
.bind(BIND_ADDRESS)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
|
@ -1,155 +1,27 @@
|
|||
use axum::{
|
||||
body::Body,
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::sse::{Event, Sse},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use futures::{stream::Stream, StreamExt, TryStreamExt};
|
||||
use mcp_server::{ByteTransport, Server};
|
||||
use std::collections::HashMap;
|
||||
use tokio_util::codec::FramedRead;
|
||||
use rmcp::transport::sse_server::SseServer;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use anyhow::Result;
|
||||
use mcp_server::router::RouterService;
|
||||
use std::sync::Arc;
|
||||
use tokio::{
|
||||
io::{self, AsyncWriteExt},
|
||||
sync::Mutex,
|
||||
};
|
||||
use tracing_subscriber::{self};
|
||||
mod common;
|
||||
use common::counter;
|
||||
|
||||
type C2SWriter = Arc<Mutex<io::WriteHalf<io::SimplexStream>>>;
|
||||
type SessionId = Arc<str>;
|
||||
use common::counter::Counter;
|
||||
|
||||
const BIND_ADDRESS: &str = "127.0.0.1:8000";
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct App {
|
||||
txs: Arc<tokio::sync::RwLock<HashMap<SessionId, C2SWriter>>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
txs: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn router(&self) -> Router {
|
||||
Router::new()
|
||||
.route("/sse", get(sse_handler).post(post_event_handler))
|
||||
.with_state(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn session_id() -> SessionId {
|
||||
let id = format!("{:016x}", rand::random::<u128>());
|
||||
Arc::from(id)
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PostEventQuery {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
async fn post_event_handler(
|
||||
State(app): State<App>,
|
||||
Query(PostEventQuery { session_id }): Query<PostEventQuery>,
|
||||
body: Body,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
const BODY_BYTES_LIMIT: usize = 1 << 22;
|
||||
let write_stream = {
|
||||
let rg = app.txs.read().await;
|
||||
rg.get(session_id.as_str())
|
||||
.ok_or(StatusCode::NOT_FOUND)?
|
||||
.clone()
|
||||
};
|
||||
let mut write_stream = write_stream.lock().await;
|
||||
let mut body = body.into_data_stream();
|
||||
if let (_, Some(size)) = body.size_hint() {
|
||||
if size > BODY_BYTES_LIMIT {
|
||||
return Err(StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
}
|
||||
// calculate the body size
|
||||
let mut size = 0;
|
||||
while let Some(chunk) = body.next().await {
|
||||
let Ok(chunk) = chunk else {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
};
|
||||
size += chunk.len();
|
||||
if size > BODY_BYTES_LIMIT {
|
||||
return Err(StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
write_stream
|
||||
.write_all(&chunk)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
}
|
||||
write_stream
|
||||
.write_u8(b'\n')
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
}
|
||||
|
||||
async fn sse_handler(State(app): State<App>) -> Sse<impl Stream<Item = Result<Event, io::Error>>> {
|
||||
// it's 4KB
|
||||
const BUFFER_SIZE: usize = 1 << 12;
|
||||
let session = session_id();
|
||||
tracing::debug!(%session, "sse connection");
|
||||
let (c2s_read, c2s_write) = tokio::io::simplex(BUFFER_SIZE);
|
||||
let (s2c_read, s2c_write) = tokio::io::simplex(BUFFER_SIZE);
|
||||
app.txs
|
||||
.write()
|
||||
.await
|
||||
.insert(session.clone(), Arc::new(Mutex::new(c2s_write)));
|
||||
{
|
||||
let session = session.clone();
|
||||
tokio::spawn(async move {
|
||||
let router = RouterService(counter::CounterRouter::new());
|
||||
let server = Server::new(router);
|
||||
let bytes_transport = ByteTransport::new(c2s_read, s2c_write);
|
||||
let _result = server
|
||||
.run(bytes_transport)
|
||||
.await
|
||||
.inspect_err(|e| tracing::error!(?e, "server run error"));
|
||||
app.txs.write().await.remove(&session);
|
||||
});
|
||||
}
|
||||
|
||||
let stream = futures::stream::once(futures::future::ok(
|
||||
Event::default()
|
||||
.event("endpoint")
|
||||
.data(format!("?sessionId={session}")),
|
||||
))
|
||||
.chain(
|
||||
FramedRead::new(s2c_read, common::jsonrpc_frame_codec::JsonRpcFrameCodec)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
.and_then(move |bytes| match std::str::from_utf8(&bytes) {
|
||||
Ok(message) => futures::future::ok(Event::default().event("message").data(message)),
|
||||
Err(e) => futures::future::err(io::Error::new(io::ErrorKind::InvalidData, e)),
|
||||
}),
|
||||
);
|
||||
Sse::new(stream)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
|
||||
.unwrap_or_else(|_| "debug".to_string().into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
let listener = tokio::net::TcpListener::bind(BIND_ADDRESS).await?;
|
||||
|
||||
tracing::debug!("listening on {}", listener.local_addr()?);
|
||||
axum::serve(listener, App::new().router()).await
|
||||
let ct = SseServer::serve(BIND_ADDRESS.parse()?)
|
||||
.await?
|
||||
.with_service(Counter::new);
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
ct.cancel();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
45
examples/servers/src/common/calculator.rs
Normal file
45
examples/servers/src/common/calculator.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use rmcp::{
|
||||
ServerHandler,
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool,
|
||||
};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct SumRequest {
|
||||
#[schemars(description = "the left hand side number")]
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calculator;
|
||||
#[tool(tool_box)]
|
||||
impl Calculator {
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
|
||||
(a + b).to_string()
|
||||
}
|
||||
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sub(
|
||||
&self,
|
||||
#[tool(param)]
|
||||
#[schemars(description = "the left hand side number")]
|
||||
a: i32,
|
||||
#[tool(param)]
|
||||
#[schemars(description = "the left hand side number")]
|
||||
b: i32,
|
||||
) -> String {
|
||||
(a - b).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(tool_box)]
|
||||
impl ServerHandler for Calculator {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
instructions: Some("A simple calculator".into()),
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +1,191 @@
|
|||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use mcp_core::{
|
||||
handler::{PromptError, ResourceError},
|
||||
prompt::{Prompt, PromptArgument},
|
||||
protocol::ServerCapabilities,
|
||||
Content, Resource, Tool, ToolError,
|
||||
use rmcp::{
|
||||
Error as McpError, RoleServer, ServerHandler, const_string, model::*, schemars,
|
||||
service::RequestContext, tool,
|
||||
};
|
||||
use mcp_server::router::CapabilitiesBuilder;
|
||||
use serde_json::Value;
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CounterRouter {
|
||||
counter: Arc<Mutex<i32>>,
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct StructRequest {
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
}
|
||||
|
||||
impl CounterRouter {
|
||||
#[derive(Clone)]
|
||||
pub struct Counter {
|
||||
counter: Arc<Mutex<i32>>,
|
||||
}
|
||||
#[tool(tool_box)]
|
||||
impl Counter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
counter: Arc::new(Mutex::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn increment(&self) -> Result<i32, ToolError> {
|
||||
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
|
||||
RawResource::new(uri, name.to_string()).no_annotation()
|
||||
}
|
||||
|
||||
#[tool(description = "Increment the counter by 1")]
|
||||
async fn increment(&self) -> Result<CallToolResult, McpError> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter += 1;
|
||||
Ok(*counter)
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
counter.to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
async fn decrement(&self) -> Result<i32, ToolError> {
|
||||
#[tool(description = "Decrement the counter by 1")]
|
||||
async fn decrement(&self) -> Result<CallToolResult, McpError> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter -= 1;
|
||||
Ok(*counter)
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
counter.to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
async fn get_value(&self) -> Result<i32, ToolError> {
|
||||
let counter = self.counter.lock().await;
|
||||
Ok(*counter)
|
||||
#[tool(description = "Get the current counter value")]
|
||||
async fn get_value(&self) -> Result<CallToolResult, McpError> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter -= 1;
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
counter.to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
|
||||
Resource::new(uri, Some("text/plain".to_string()), Some(name.to_string())).unwrap()
|
||||
#[tool(description = "Say hello to the client")]
|
||||
fn say_hello(&self) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![Content::text("hello")]))
|
||||
}
|
||||
|
||||
#[tool(description = "Repeat what you say")]
|
||||
fn echo(
|
||||
&self,
|
||||
#[tool(param)]
|
||||
#[schemars(description = "Repeat what you say")]
|
||||
saying: String,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![Content::text(saying)]))
|
||||
}
|
||||
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sum(
|
||||
&self,
|
||||
#[tool(aggr)] StructRequest { a, b }: StructRequest,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
(a + b).to_string(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
|
||||
impl mcp_server::Router for CounterRouter {
|
||||
fn name(&self) -> String {
|
||||
"counter".to_string()
|
||||
const_string!(Echo = "echo");
|
||||
#[tool(tool_box)]
|
||||
impl ServerHandler for Counter {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: ProtocolVersion::V_2024_11_05,
|
||||
capabilities: ServerCapabilities::builder()
|
||||
.enable_prompts()
|
||||
.enable_resources()
|
||||
.enable_tools()
|
||||
.build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some("This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn instructions(&self) -> String {
|
||||
"This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string()
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ServerCapabilities {
|
||||
CapabilitiesBuilder::new()
|
||||
.with_tools(false)
|
||||
.with_resources(false, false)
|
||||
.with_prompts(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn list_tools(&self) -> Vec<Tool> {
|
||||
vec![
|
||||
Tool::new(
|
||||
"increment".to_string(),
|
||||
"Increment the counter by 1".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"decrement".to_string(),
|
||||
"Decrement the counter by 1".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"get_value".to_string(),
|
||||
"Get the current counter value".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn call_tool(
|
||||
async fn list_resources(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
_arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
|
||||
let this = self.clone();
|
||||
let tool_name = tool_name.to_string();
|
||||
|
||||
Box::pin(async move {
|
||||
match tool_name.as_str() {
|
||||
"increment" => {
|
||||
let value = this.increment().await?;
|
||||
Ok(vec![Content::text(value.to_string())])
|
||||
}
|
||||
"decrement" => {
|
||||
let value = this.decrement().await?;
|
||||
Ok(vec![Content::text(value.to_string())])
|
||||
}
|
||||
"get_value" => {
|
||||
let value = this.get_value().await?;
|
||||
Ok(vec![Content::text(value.to_string())])
|
||||
}
|
||||
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
|
||||
}
|
||||
_request: PaginatedRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListResourcesResult, McpError> {
|
||||
Ok(ListResourcesResult {
|
||||
resources: vec![
|
||||
self._create_resource_text("str:////Users/to/some/path/", "cwd"),
|
||||
self._create_resource_text("memo://insights", "memo-name"),
|
||||
],
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn list_resources(&self) -> Vec<Resource> {
|
||||
vec![
|
||||
self._create_resource_text("str:////Users/to/some/path/", "cwd"),
|
||||
self._create_resource_text("memo://insights", "memo-name"),
|
||||
]
|
||||
async fn read_resource(
|
||||
&self,
|
||||
ReadResourceRequestParam { uri }: ReadResourceRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ReadResourceResult, McpError> {
|
||||
match uri.as_str() {
|
||||
"str:////Users/to/some/path/" => {
|
||||
let cwd = "/Users/to/some/path/";
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::text(cwd, uri)],
|
||||
})
|
||||
}
|
||||
"memo://insights" => {
|
||||
let memo = "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...";
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::text(memo, uri)],
|
||||
})
|
||||
}
|
||||
_ => Err(McpError::resource_not_found(
|
||||
"resource_not_found",
|
||||
Some(json!({
|
||||
"uri": uri
|
||||
})),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_resource(
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
uri: &str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
|
||||
let uri = uri.to_string();
|
||||
Box::pin(async move {
|
||||
match uri.as_str() {
|
||||
"str:////Users/to/some/path/" => {
|
||||
let cwd = "/Users/to/some/path/";
|
||||
Ok(cwd.to_string())
|
||||
}
|
||||
"memo://insights" => {
|
||||
let memo =
|
||||
"Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...";
|
||||
Ok(memo.to_string())
|
||||
}
|
||||
_ => Err(ResourceError::NotFound(format!(
|
||||
"Resource {} not found",
|
||||
uri
|
||||
))),
|
||||
}
|
||||
_request: PaginatedRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListPromptsResult, McpError> {
|
||||
Ok(ListPromptsResult {
|
||||
next_cursor: None,
|
||||
prompts: vec![Prompt::new(
|
||||
"example_prompt",
|
||||
Some("This is an example prompt that takes one required agrument, message"),
|
||||
Some(vec![PromptArgument {
|
||||
name: "message".to_string(),
|
||||
description: Some("A message to put in the prompt".to_string()),
|
||||
required: Some(true),
|
||||
}]),
|
||||
)],
|
||||
})
|
||||
}
|
||||
|
||||
fn list_prompts(&self) -> Vec<Prompt> {
|
||||
vec![Prompt::new(
|
||||
"example_prompt",
|
||||
Some("This is an example prompt that takes one required agrument, message"),
|
||||
Some(vec![PromptArgument {
|
||||
name: "message".to_string(),
|
||||
description: Some("A message to put in the prompt".to_string()),
|
||||
required: Some(true),
|
||||
}]),
|
||||
)]
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
GetPromptRequestParam { name, arguments: _ }: GetPromptRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<GetPromptResult, McpError> {
|
||||
match name.as_str() {
|
||||
"example_prompt" => {
|
||||
let prompt = "This is an example prompt with your message here: '{message}'";
|
||||
Ok(GetPromptResult {
|
||||
description: None,
|
||||
messages: vec![PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: PromptMessageContent::text(prompt),
|
||||
}],
|
||||
})
|
||||
}
|
||||
_ => Err(McpError::invalid_params("prompt not found", None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_prompt(
|
||||
async fn list_resource_templates(
|
||||
&self,
|
||||
prompt_name: &str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String, PromptError>> + Send + 'static>> {
|
||||
let prompt_name = prompt_name.to_string();
|
||||
Box::pin(async move {
|
||||
match prompt_name.as_str() {
|
||||
"example_prompt" => {
|
||||
let prompt = "This is an example prompt with your message here: '{message}'";
|
||||
Ok(prompt.to_string())
|
||||
}
|
||||
_ => Err(PromptError::NotFound(format!(
|
||||
"Prompt {} not found",
|
||||
prompt_name
|
||||
))),
|
||||
}
|
||||
_request: PaginatedRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListResourceTemplatesResult, McpError> {
|
||||
Ok(ListResourceTemplatesResult {
|
||||
next_cursor: None,
|
||||
resource_templates: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
use tokio_util::codec::Decoder;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsonRpcFrameCodec;
|
||||
impl Decoder for JsonRpcFrameCodec {
|
||||
type Item = tokio_util::bytes::Bytes;
|
||||
type Error = tokio::io::Error;
|
||||
fn decode(
|
||||
&mut self,
|
||||
src: &mut tokio_util::bytes::BytesMut,
|
||||
) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if let Some(end) = src
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, &b)| (b == b'\n').then_some(idx))
|
||||
{
|
||||
let line = src.split_to(end);
|
||||
let _char_next_line = src.split_to(1);
|
||||
Ok(Some(line.freeze()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
pub mod calculator;
|
||||
pub mod counter;
|
||||
pub mod jsonrpc_frame_codec;
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use mcp_server::router::RouterService;
|
||||
use mcp_server::{ByteTransport, Server};
|
||||
use tokio::io::{stdin, stdout};
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::{self, EnvFilter};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Set up file appender for logging
|
||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "mcp-server.log");
|
||||
|
||||
// Initialize the tracing subscriber with file and stdout logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
|
||||
.with_writer(file_appender)
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.init();
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
|
||||
// Create an instance of our counter router
|
||||
let router = RouterService(common::counter::CounterRouter::new());
|
||||
|
||||
// Create and run the server
|
||||
let server = Server::new(router);
|
||||
let transport = ByteTransport::new(stdin(), stdout());
|
||||
|
||||
tracing::info!("Server initialized and ready to handle requests");
|
||||
Ok(server.run(transport).await?)
|
||||
}
|
||||
26
examples/servers/src/std_io.rs
Normal file
26
examples/servers/src/std_io.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use anyhow::Result;
|
||||
use common::counter::Counter;
|
||||
use rmcp::{ServiceExt, transport::stdio};
|
||||
|
||||
use tracing_subscriber::{self, EnvFilter};
|
||||
mod common;
|
||||
/// npx @modelcontextprotocol/inspector cargo run -p mcp-server-examples --example std_io
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize the tracing subscriber with file and stdout logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
|
||||
.with_writer(std::io::stderr)
|
||||
.with_ansi(false)
|
||||
.init();
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
|
||||
// Create an instance of our counter router
|
||||
let service = Counter::new().serve(stdio()).await.inspect_err(|e| {
|
||||
tracing::error!("serving error: {:?}", e);
|
||||
})?;
|
||||
|
||||
service.waiting().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
//! build: cargo build -p mcp-server-examples --example wasi_std_io --target wasm32-wasip1
|
||||
//!
|
||||
//! run: npx @modelcontextprotocol/inspector wasmedge --dir logs:. run target/wasm32-wasip1/debug/examples/wasi_std_io.wasm
|
||||
//!
|
||||
use mcp_server::{router::RouterService, ByteTransport, Server};
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
mod common;
|
||||
use anyhow::Result;
|
||||
use common::counter::CounterRouter;
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
// Set up file appender for logging
|
||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "mcp-server.log");
|
||||
|
||||
// Initialize the tracing subscriber with file and stdout logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
|
||||
.with_writer(file_appender)
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.init();
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
|
||||
// Create an instance of our counter router
|
||||
let router = RouterService(CounterRouter::new());
|
||||
|
||||
// Create and run the server
|
||||
let server = Server::new(router);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let transport = ByteTransport::new(async_io::WasiFd::std_in(), async_io::WasiFd::std_out());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let transport = ByteTransport::new(tokio::io::stdin(), tokio::io::stdout());
|
||||
|
||||
tracing::info!("Server initialized and ready to handle requests");
|
||||
Ok(server.run(transport).await?)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod async_io {
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use wasi::{Fd, FD_STDIN, FD_STDOUT};
|
||||
|
||||
pub struct WasiFd {
|
||||
fd: Fd,
|
||||
}
|
||||
|
||||
impl WasiFd {
|
||||
pub fn std_in() -> Self {
|
||||
Self { fd: FD_STDIN }
|
||||
}
|
||||
pub fn std_out() -> Self {
|
||||
Self { fd: FD_STDOUT }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for WasiFd {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
let mut temp_buf = vec![0u8; buf.remaining()];
|
||||
unsafe {
|
||||
match wasi::fd_read(
|
||||
self.fd,
|
||||
&[wasi::Iovec {
|
||||
buf: temp_buf.as_mut_ptr(),
|
||||
buf_len: temp_buf.len(),
|
||||
}],
|
||||
) {
|
||||
Ok(n) => {
|
||||
buf.put_slice(&temp_buf[..n]);
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
Err(err) => std::task::Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("WASI read error: {}", err),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for WasiFd {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
||||
unsafe {
|
||||
match wasi::fd_write(
|
||||
self.fd,
|
||||
&[wasi::Ciovec {
|
||||
buf: buf.as_ptr(),
|
||||
buf_len: buf.len(),
|
||||
}],
|
||||
) {
|
||||
Ok(n) => std::task::Poll::Ready(Ok(n)),
|
||||
Err(err) => std::task::Poll::Ready(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("WASI write error: {}", err),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
std::task::Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
self.poll_flush(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue