Skip to content

Commit 07b7230

Browse files
feat: Introduce a new WASM server architecture with durable objects, streaming transport, enhanced authentication, and CLI tooling.
1 parent cc2fb7e commit 07b7230

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+16000
-384
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ members = [
2626
"crates/turbomcp-wasm-macros", # v3.0: WASM server proc macros
2727
"crates/turbomcp-wire", # v3.0: Wire format codec abstraction
2828
"crates/turbomcp-openapi", # v3.0: OpenAPI to MCP conversion
29+
"crates/turbomcp-transport-streamable", # v3.0: Streamable HTTP transport types
2930
"demo",
3031
]
3132
resolver = "2"
@@ -221,6 +222,8 @@ turbomcp-dpop = { version = "3.0.0-beta.4", path = "crates/turbomcp-dpop" }
221222
turbomcp-wire = { version = "3.0.0-beta.4", path = "crates/turbomcp-wire", default-features = false }
222223
# v3.0: WASM server proc macros
223224
turbomcp-wasm-macros = { version = "3.0.0-beta.4", path = "crates/turbomcp-wasm-macros" }
225+
# v3.0: Streamable HTTP transport types (SSE, sessions, MCP 2025-11-25)
226+
turbomcp-transport-streamable = { version = "3.0.0-beta.4", path = "crates/turbomcp-transport-streamable", default-features = false }
224227

225228
[profile.dev]
226229
opt-level = 1

crates/turbomcp-cli/src/build.rs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
//! Build command implementation for MCP servers.
2+
//!
3+
//! Supports building for native and WASM targets, with platform-specific
4+
//! optimizations for Cloudflare Workers and other edge runtimes.
5+
6+
use crate::cli::{BuildArgs, WasmPlatform};
7+
use crate::error::{CliError, CliResult};
8+
use std::path::Path;
9+
use std::process::Command;
10+
11+
/// Execute the build command.
12+
pub fn execute(args: &BuildArgs) -> CliResult<()> {
13+
let project_path = args.path.canonicalize().map_err(|e| {
14+
CliError::Other(format!(
15+
"Failed to resolve project path '{}': {}",
16+
args.path.display(),
17+
e
18+
))
19+
})?;
20+
21+
// Check if Cargo.toml exists
22+
let cargo_toml = project_path.join("Cargo.toml");
23+
if !cargo_toml.exists() {
24+
return Err(CliError::Other(format!(
25+
"No Cargo.toml found at '{}'",
26+
project_path.display()
27+
)));
28+
}
29+
30+
// Determine target based on platform or explicit target
31+
let target = determine_target(args)?;
32+
33+
println!("Building MCP server...");
34+
if let Some(ref t) = target {
35+
println!(" Target: {}", t);
36+
}
37+
if args.release {
38+
println!(" Mode: release");
39+
} else {
40+
println!(" Mode: debug");
41+
}
42+
43+
// Build the cargo command
44+
let mut cmd = Command::new("cargo");
45+
cmd.arg("build");
46+
cmd.current_dir(&project_path);
47+
48+
// Add target if specified
49+
if let Some(ref t) = target {
50+
cmd.arg("--target").arg(t);
51+
}
52+
53+
// Release mode
54+
if args.release {
55+
cmd.arg("--release");
56+
}
57+
58+
// Features
59+
if args.no_default_features {
60+
cmd.arg("--no-default-features");
61+
}
62+
63+
for feature in &args.features {
64+
cmd.arg("--features").arg(feature);
65+
}
66+
67+
// Execute cargo build
68+
let status = cmd
69+
.status()
70+
.map_err(|e| CliError::Other(format!("Failed to execute cargo build: {}", e)))?;
71+
72+
if !status.success() {
73+
return Err(CliError::Other("Cargo build failed".to_string()));
74+
}
75+
76+
println!("Build successful!");
77+
78+
// Determine output path
79+
let profile = if args.release { "release" } else { "debug" };
80+
let target_dir = project_path.join("target");
81+
82+
let output_dir = if let Some(ref t) = target {
83+
target_dir.join(t).join(profile)
84+
} else {
85+
target_dir.join(profile)
86+
};
87+
88+
// For WASM targets, run wasm-opt if requested
89+
if args.optimize && target.as_ref().is_some_and(|t| t.contains("wasm")) {
90+
optimize_wasm(&output_dir, args)?;
91+
}
92+
93+
// Copy to output directory if specified
94+
if let Some(ref output) = args.output {
95+
copy_artifacts(&output_dir, output, &target)?;
96+
}
97+
98+
// Print output location
99+
if let Some(ref output) = args.output {
100+
println!("Artifacts copied to: {}", output.display());
101+
} else {
102+
println!("Artifacts at: {}", output_dir.display());
103+
}
104+
105+
Ok(())
106+
}
107+
108+
/// Determine the Rust target based on platform or explicit target argument.
109+
fn determine_target(args: &BuildArgs) -> CliResult<Option<String>> {
110+
// Explicit target takes precedence
111+
if let Some(ref target) = args.target {
112+
return Ok(Some(target.clone()));
113+
}
114+
115+
// Platform-specific targets
116+
if let Some(ref platform) = args.platform {
117+
let target = match platform {
118+
WasmPlatform::CloudflareWorkers | WasmPlatform::DenoWorkers | WasmPlatform::Wasm32 => {
119+
"wasm32-unknown-unknown"
120+
}
121+
};
122+
return Ok(Some(target.to_string()));
123+
}
124+
125+
// No target specified - build for native
126+
Ok(None)
127+
}
128+
129+
/// Optimize WASM binary using wasm-opt.
130+
fn optimize_wasm(output_dir: &Path, args: &BuildArgs) -> CliResult<()> {
131+
// Check if wasm-opt is available
132+
let wasm_opt_check = Command::new("wasm-opt").arg("--version").output();
133+
134+
if wasm_opt_check.is_err() {
135+
println!("Warning: wasm-opt not found, skipping optimization");
136+
println!(" Install with: cargo install wasm-opt");
137+
return Ok(());
138+
}
139+
140+
println!("Optimizing WASM binary...");
141+
142+
// Find all .wasm files in the output directory
143+
let wasm_files: Vec<_> = std::fs::read_dir(output_dir)
144+
.map_err(|e| CliError::Other(format!("Failed to read output directory: {}", e)))?
145+
.filter_map(|entry| entry.ok())
146+
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "wasm"))
147+
.collect();
148+
149+
for entry in wasm_files {
150+
let wasm_path = entry.path();
151+
let optimized_path = wasm_path.with_extension("optimized.wasm");
152+
153+
let opt_level = if args.release { "-O3" } else { "-O1" };
154+
155+
let status = Command::new("wasm-opt")
156+
.arg(opt_level)
157+
.arg("-o")
158+
.arg(&optimized_path)
159+
.arg(&wasm_path)
160+
.status()
161+
.map_err(|e| CliError::Other(format!("Failed to run wasm-opt: {}", e)))?;
162+
163+
if status.success() {
164+
// Replace original with optimized
165+
std::fs::rename(&optimized_path, &wasm_path)
166+
.map_err(|e| CliError::Other(format!("Failed to replace WASM file: {}", e)))?;
167+
168+
// Get file size for reporting
169+
let metadata = std::fs::metadata(&wasm_path)
170+
.map_err(|e| CliError::Other(format!("Failed to get file metadata: {}", e)))?;
171+
let size_kb = metadata.len() / 1024;
172+
173+
println!(" Optimized: {} ({}KB)", wasm_path.display(), size_kb);
174+
} else {
175+
println!("Warning: wasm-opt failed for {}", wasm_path.display());
176+
}
177+
}
178+
179+
Ok(())
180+
}
181+
182+
/// Copy build artifacts to the specified output directory.
183+
fn copy_artifacts(source_dir: &Path, output_dir: &Path, target: &Option<String>) -> CliResult<()> {
184+
// Create output directory
185+
std::fs::create_dir_all(output_dir)
186+
.map_err(|e| CliError::Other(format!("Failed to create output directory: {}", e)))?;
187+
188+
// Determine which files to copy based on target
189+
let is_wasm = target.as_ref().is_some_and(|t| t.contains("wasm"));
190+
191+
if is_wasm {
192+
// Copy .wasm files
193+
for entry in std::fs::read_dir(source_dir)
194+
.map_err(|e| CliError::Other(format!("Failed to read source directory: {}", e)))?
195+
{
196+
let entry =
197+
entry.map_err(|e| CliError::Other(format!("Failed to read entry: {}", e)))?;
198+
let path = entry.path();
199+
200+
if path.extension().is_some_and(|ext| ext == "wasm") {
201+
let dest = output_dir.join(path.file_name().unwrap());
202+
std::fs::copy(&path, &dest)
203+
.map_err(|e| CliError::Other(format!("Failed to copy file: {}", e)))?;
204+
}
205+
}
206+
} else {
207+
// Copy binary files (no extension on Unix, .exe on Windows)
208+
for entry in std::fs::read_dir(source_dir)
209+
.map_err(|e| CliError::Other(format!("Failed to read source directory: {}", e)))?
210+
{
211+
let entry =
212+
entry.map_err(|e| CliError::Other(format!("Failed to read entry: {}", e)))?;
213+
let path = entry.path();
214+
215+
if path.is_file() {
216+
let is_binary = if cfg!(windows) {
217+
path.extension().is_some_and(|ext| ext == "exe")
218+
} else {
219+
path.extension().is_none()
220+
&& std::fs::metadata(&path)
221+
.map(|m| m.permissions().mode() & 0o111 != 0)
222+
.unwrap_or(false)
223+
};
224+
225+
if is_binary {
226+
let dest = output_dir.join(path.file_name().unwrap());
227+
std::fs::copy(&path, &dest)
228+
.map_err(|e| CliError::Other(format!("Failed to copy file: {}", e)))?;
229+
}
230+
}
231+
}
232+
}
233+
234+
Ok(())
235+
}
236+
237+
#[cfg(unix)]
238+
use std::os::unix::fs::PermissionsExt;
239+
240+
#[cfg(not(unix))]
241+
trait PermissionsExt {
242+
fn mode(&self) -> u32 {
243+
0
244+
}
245+
}
246+
247+
#[cfg(not(unix))]
248+
impl PermissionsExt for std::fs::Permissions {}
249+
250+
#[cfg(test)]
251+
mod tests {
252+
use super::*;
253+
254+
#[test]
255+
fn test_determine_target_explicit() {
256+
let args = BuildArgs {
257+
path: ".".into(),
258+
platform: None,
259+
target: Some("x86_64-unknown-linux-gnu".to_string()),
260+
release: false,
261+
optimize: false,
262+
features: vec![],
263+
no_default_features: false,
264+
output: None,
265+
};
266+
267+
let target = determine_target(&args).unwrap();
268+
assert_eq!(target, Some("x86_64-unknown-linux-gnu".to_string()));
269+
}
270+
271+
#[test]
272+
fn test_determine_target_platform() {
273+
let args = BuildArgs {
274+
path: ".".into(),
275+
platform: Some(WasmPlatform::CloudflareWorkers),
276+
target: None,
277+
release: false,
278+
optimize: false,
279+
features: vec![],
280+
no_default_features: false,
281+
output: None,
282+
};
283+
284+
let target = determine_target(&args).unwrap();
285+
assert_eq!(target, Some("wasm32-unknown-unknown".to_string()));
286+
}
287+
288+
#[test]
289+
fn test_determine_target_none() {
290+
let args = BuildArgs {
291+
path: ".".into(),
292+
platform: None,
293+
target: None,
294+
release: false,
295+
optimize: false,
296+
features: vec![],
297+
no_default_features: false,
298+
output: None,
299+
};
300+
301+
let target = determine_target(&args).unwrap();
302+
assert_eq!(target, None);
303+
}
304+
}

0 commit comments

Comments
 (0)