Server wasm compatibility (#15)

* add axum sse example

* fix: make the sse axum folder name correct

* feat: add common modules for Axum SSE example

This commit adds supporting modules for the Axum Server-Sent Events (SSE) example:
- Added `counter.rs` with a simple counter router implementation
- Added `jsonrpc_frame_codec.rs` for decoding JSON-RPC frames
- Created a `mod.rs` to expose these modules
- Removed the previous SSE example configuration from Cargo.toml

* refactor: remove main.rs and update Cargo.toml for WASM compatibility

This commit makes two key changes:
1. Removes the main.rs file and make it a example
2. Add a wasi_std_io example

* refactor: simplify WASI I/O handling in example and fmt

Merged AsyncInputStream and AsyncOutputStream into a single WasiFd struct with std_in() and std_out() methods, reducing code duplication and improving clarity of the WASI standard I/O example.

* move examples to root
This commit is contained in:
4t145 2025-03-15 02:21:33 +08:00 committed by GitHub
parent c0bd94dd85
commit 4597d1f047
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 141 additions and 4 deletions

View file

@ -14,7 +14,7 @@ mcp-macros = { workspace = true }
serde = { version = "1.0.216", features = ["derive"] } serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0.133" serde_json = "1.0.133"
schemars = "0.8" schemars = "0.8"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["io-util"] }
tower = { version = "0.4", features = ["timeout"] } tower = { version = "0.4", features = ["timeout"] }
tower-service = "0.3" tower-service = "0.3"
futures = "0.3" futures = "0.3"

View file

@ -8,7 +8,7 @@ publish = false
mcp-server = { path = "../../crates/mcp-server" } mcp-server = { path = "../../crates/mcp-server" }
mcp-core = { path = "../../crates/mcp-core" } mcp-core = { path = "../../crates/mcp-core" }
mcp-macros = { path = "../../crates/mcp-macros" } mcp-macros = { path = "../../crates/mcp-macros" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["io-util"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
anyhow = "1.0" anyhow = "1.0"
@ -18,14 +18,26 @@ tracing-appender = "0.2"
futures = "0.3" futures = "0.3"
[dev-dependencies] [dev-dependencies]
axum = { version = "0.8", features = ["macros"] }
tokio-util = { version = "0.7", features = ["io", "codec"]} tokio-util = { version = "0.7", features = ["io", "codec"]}
rand = { version = "0.8" } rand = { version = "0.8" }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
axum = { version = "0.8", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1", features = ["io-util", "rt", "time", "macros"] }
wasi = { version = "0.11.0+wasi-snapshot-preview1" }
[[example]] [[example]]
name = "counter-server" name = "counter-server"
path = "src/counter_server.rs" path = "src/counter_server.rs"
[[example]] [[example]]
name = "axum" name = "axum"
path = "src/axum.rs" path = "src/axum.rs"
[[example]]
name = "wasi_std_io"
path = "src/wasi_std_io.rs"

View file

@ -0,0 +1,125 @@
//! 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)
}
}
}