diff --git a/crates/mcp-server/Cargo.toml b/crates/mcp-server/Cargo.toml index 6baa7fe8..6eb5703c 100644 --- a/crates/mcp-server/Cargo.toml +++ b/crates/mcp-server/Cargo.toml @@ -14,7 +14,7 @@ mcp-macros = { workspace = true } serde = { version = "1.0.216", features = ["derive"] } serde_json = "1.0.133" schemars = "0.8" -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["io-util"] } tower = { version = "0.4", features = ["timeout"] } tower-service = "0.3" futures = "0.3" diff --git a/examples/servers/Cargo.toml b/examples/servers/Cargo.toml index c8d47783..b0b58ae3 100644 --- a/examples/servers/Cargo.toml +++ b/examples/servers/Cargo.toml @@ -8,7 +8,7 @@ publish = false mcp-server = { path = "../../crates/mcp-server" } mcp-core = { path = "../../crates/mcp-core" } mcp-macros = { path = "../../crates/mcp-macros" } -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["io-util"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" @@ -18,14 +18,26 @@ tracing-appender = "0.2" futures = "0.3" [dev-dependencies] -axum = { version = "0.8", features = ["macros"] } + 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"] } + +[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]] name = "counter-server" path = "src/counter_server.rs" [[example]] name = "axum" -path = "src/axum.rs" \ No newline at end of file +path = "src/axum.rs" + +[[example]] +name = "wasi_std_io" +path = "src/wasi_std_io.rs" \ No newline at end of file diff --git a/examples/servers/src/wasi_std_io.rs b/examples/servers/src/wasi_std_io.rs new file mode 100644 index 00000000..b2e71f77 --- /dev/null +++ b/examples/servers/src/wasi_std_io.rs @@ -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> { + 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> { + 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> { + std::task::Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.poll_flush(cx) + } + } +}