Skip to content

Server wasm compatibility #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 14, 2025
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
2 changes: 1 addition & 1 deletion crates/mcp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 15 additions & 3 deletions examples/servers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
path = "src/axum.rs"

[[example]]
name = "wasi_std_io"
path = "src/wasi_std_io.rs"
125 changes: 125 additions & 0 deletions examples/servers/src/wasi_std_io.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}