1use crate::cli::{NewArgs, ProjectTemplate};
7use crate::error::{CliError, CliResult};
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12pub fn execute(args: &NewArgs) -> CliResult<()> {
14 let output_dir = args
16 .output
17 .clone()
18 .unwrap_or_else(|| PathBuf::from(&args.name));
19
20 if output_dir.exists() {
22 return Err(CliError::Other(format!(
23 "Directory '{}' already exists",
24 output_dir.display()
25 )));
26 }
27
28 println!("Creating new MCP server project '{}'...", args.name);
29 println!(" Template: {}", args.template);
30
31 fs::create_dir_all(&output_dir)
33 .map_err(|e| CliError::Other(format!("Failed to create directory: {}", e)))?;
34
35 match args.template {
37 ProjectTemplate::Minimal => generate_minimal(args, &output_dir)?,
38 ProjectTemplate::Full => generate_full(args, &output_dir)?,
39 ProjectTemplate::CloudflareWorkers => generate_cloudflare_workers(args, &output_dir)?,
40 ProjectTemplate::CloudflareWorkersOauth => {
41 generate_cloudflare_workers_oauth(args, &output_dir)?
42 }
43 ProjectTemplate::CloudflareWorkersDurableObjects => {
44 generate_cloudflare_workers_do(args, &output_dir)?
45 }
46 }
47
48 if args.git {
50 init_git(&output_dir)?;
51 }
52
53 println!("\nProject created successfully!");
54 println!("\nNext steps:");
55 println!(" cd {}", args.name);
56
57 match args.template {
58 ProjectTemplate::CloudflareWorkers
59 | ProjectTemplate::CloudflareWorkersOauth
60 | ProjectTemplate::CloudflareWorkersDurableObjects => {
61 println!(" npx wrangler dev # Start local development");
62 println!(" npx wrangler deploy # Deploy to Cloudflare");
63 }
64 _ => {
65 println!(" cargo build # Build the server");
66 println!(" cargo run # Run the server");
67 }
68 }
69
70 Ok(())
71}
72
73fn generate_minimal(args: &NewArgs, output_dir: &Path) -> CliResult<()> {
75 let description = args
76 .description
77 .as_deref()
78 .unwrap_or("A minimal MCP server");
79
80 let cargo_toml = format!(
82 r#"[package]
83name = "{name}"
84version = "0.1.0"
85edition = "2024"
86description = "{description}"
87{author}
88
89[dependencies]
90turbomcp = "3.0"
91tokio = {{ version = "1", features = ["full"] }}
92serde = {{ version = "1", features = ["derive"] }}
93schemars = "0.8"
94"#,
95 name = args.name,
96 description = description,
97 author = args
98 .author
99 .as_ref()
100 .map(|a| format!("authors = [\"{}\"]", a))
101 .unwrap_or_default(),
102 );
103
104 let main_rs = format!(
106 r#"//! {description}
107
108use turbomcp::prelude::*;
109use serde::Deserialize;
110
111#[derive(Clone)]
112struct {struct_name};
113
114#[derive(Deserialize, schemars::JsonSchema)]
115struct HelloArgs {{
116 /// Name to greet
117 name: String,
118}}
119
120#[server(name = "{name}", version = "0.1.0")]
121impl {struct_name} {{
122 /// Say hello to someone
123 #[tool("Say hello to someone")]
124 async fn hello(&self, args: HelloArgs) -> String {{
125 format!("Hello, {{}}!", args.name)
126 }}
127}}
128
129#[tokio::main]
130async fn main() -> Result<(), Box<dyn std::error::Error>> {{
131 let server = {struct_name};
132 server.serve_stdio().await?;
133 Ok(())
134}}
135"#,
136 description = description,
137 struct_name = to_struct_name(&args.name),
138 name = args.name,
139 );
140
141 write_file(output_dir, "Cargo.toml", &cargo_toml)?;
143 fs::create_dir_all(output_dir.join("src"))?;
144 write_file(&output_dir.join("src"), "main.rs", &main_rs)?;
145
146 write_file(output_dir, ".gitignore", "/target\n")?;
148
149 Ok(())
150}
151
152fn generate_full(args: &NewArgs, output_dir: &Path) -> CliResult<()> {
154 let description = args
155 .description
156 .as_deref()
157 .unwrap_or("A full-featured MCP server");
158
159 let cargo_toml = format!(
161 r#"[package]
162name = "{name}"
163version = "0.1.0"
164edition = "2024"
165description = "{description}"
166{author}
167
168[dependencies]
169turbomcp = {{ version = "3.0", features = ["http", "auth"] }}
170tokio = {{ version = "1", features = ["full"] }}
171serde = {{ version = "1", features = ["derive"] }}
172serde_json = "1"
173schemars = "0.8"
174tracing = "0.1"
175tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
176"#,
177 name = args.name,
178 description = description,
179 author = args
180 .author
181 .as_ref()
182 .map(|a| format!("authors = [\"{}\"]", a))
183 .unwrap_or_default(),
184 );
185
186 let main_rs = format!(
188 r#"//! {description}
189
190use turbomcp::prelude::*;
191use serde::{{Deserialize, Serialize}};
192use tracing_subscriber::{{layer::SubscriberExt, util::SubscriberInitExt}};
193
194#[derive(Clone)]
195struct {struct_name} {{
196 config: ServerConfig,
197}}
198
199#[derive(Clone, Debug, Serialize, Deserialize)]
200struct ServerConfig {{
201 name: String,
202}}
203
204// Tool argument types
205#[derive(Deserialize, schemars::JsonSchema)]
206struct HelloArgs {{
207 /// Name to greet
208 name: String,
209}}
210
211#[derive(Deserialize, schemars::JsonSchema)]
212struct CalculateArgs {{
213 /// First number
214 a: f64,
215 /// Second number
216 b: f64,
217 /// Operation to perform
218 operation: String,
219}}
220
221// Prompt argument types
222#[derive(Deserialize, schemars::JsonSchema)]
223struct GreetingArgs {{
224 /// User's name
225 name: String,
226 /// Tone of greeting
227 tone: Option<String>,
228}}
229
230#[server(
231 name = "{name}",
232 version = "0.1.0",
233 description = "{description}"
234)]
235impl {struct_name} {{
236 /// Say hello to someone
237 #[tool("Say hello to someone")]
238 async fn hello(&self, args: HelloArgs) -> String {{
239 format!("Hello, {{}}!", args.name)
240 }}
241
242 /// Perform a calculation
243 #[tool("Perform basic arithmetic")]
244 async fn calculate(&self, args: CalculateArgs) -> Result<String, ToolError> {{
245 let result = match args.operation.as_str() {{
246 "add" => args.a + args.b,
247 "subtract" => args.a - args.b,
248 "multiply" => args.a * args.b,
249 "divide" => {{
250 if args.b == 0.0 {{
251 return Err(ToolError::new("Cannot divide by zero"));
252 }}
253 args.a / args.b
254 }}
255 _ => return Err(ToolError::new(format!(
256 "Unknown operation: {{}}. Use: add, subtract, multiply, divide",
257 args.operation
258 ))),
259 }};
260 Ok(format!("{{}} {{}} {{}} = {{}}", args.a, args.operation, args.b, result))
261 }}
262
263 /// Server configuration
264 #[resource("config://server")]
265 async fn config(&self, _uri: String) -> ResourceResult {{
266 ResourceResult::json(
267 "config://server",
268 &self.config,
269 ).map_err(|e| ResourceError::new(e.to_string()))
270 .unwrap_or_else(|e| ResourceResult::text("config://server", &format!("Error: {{}}", e)))
271 }}
272
273 /// Greeting prompt
274 #[prompt("Generate a greeting")]
275 async fn greeting(&self, args: GreetingArgs) -> PromptResult {{
276 let tone = args.tone.as_deref().unwrap_or("friendly");
277 PromptResult::user(format!(
278 "Generate a {{}} greeting for {{}}.",
279 tone,
280 args.name
281 ))
282 }}
283}}
284
285#[tokio::main]
286async fn main() -> Result<(), Box<dyn std::error::Error>> {{
287 // Initialize tracing
288 tracing_subscriber::registry()
289 .with(tracing_subscriber::EnvFilter::new(
290 std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
291 ))
292 .with(tracing_subscriber::fmt::layer())
293 .init();
294
295 let server = {struct_name} {{
296 config: ServerConfig {{
297 name: "{name}".to_string(),
298 }},
299 }};
300
301 // Choose transport based on environment
302 if std::env::var("MCP_HTTP").is_ok() {{
303 tracing::info!("Starting HTTP server on port 8080...");
304 server.serve_http(8080).await?;
305 }} else {{
306 tracing::info!("Starting STDIO server...");
307 server.serve_stdio().await?;
308 }}
309
310 Ok(())
311}}
312"#,
313 description = description,
314 struct_name = to_struct_name(&args.name),
315 name = args.name,
316 );
317
318 write_file(output_dir, "Cargo.toml", &cargo_toml)?;
320 fs::create_dir_all(output_dir.join("src"))?;
321 write_file(&output_dir.join("src"), "main.rs", &main_rs)?;
322
323 write_file(output_dir, ".gitignore", "/target\n")?;
325
326 Ok(())
327}
328
329fn generate_cloudflare_workers(args: &NewArgs, output_dir: &Path) -> CliResult<()> {
331 let description = args
332 .description
333 .as_deref()
334 .unwrap_or("An MCP server for Cloudflare Workers");
335
336 let cargo_toml = format!(
338 r#"[package]
339name = "{name}"
340version = "0.1.0"
341edition = "2024"
342description = "{description}"
343{author}
344
345[lib]
346crate-type = ["cdylib"]
347
348[dependencies]
349turbomcp-wasm = {{ version = "3.0", features = ["macros", "streamable"] }}
350worker = "0.5"
351serde = {{ version = "1", features = ["derive"] }}
352serde_json = "1"
353schemars = "0.8"
354wasm-bindgen = "0.2"
355wasm-bindgen-futures = "0.4"
356
357[profile.release]
358opt-level = "s"
359lto = true
360"#,
361 name = args.name,
362 description = description,
363 author = args
364 .author
365 .as_ref()
366 .map(|a| format!("authors = [\"{}\"]", a))
367 .unwrap_or_default(),
368 );
369
370 let wrangler_toml = format!(
372 r#"name = "{name}"
373main = "build/worker/shim.mjs"
374compatibility_date = "2024-01-01"
375
376[build]
377command = "cargo install -q worker-build && worker-build --release"
378
379# Uncomment for KV storage
380# [[kv_namespaces]]
381# binding = "MY_KV"
382# id = "your-kv-namespace-id"
383
384# Uncomment for Durable Objects
385# [[durable_objects.bindings]]
386# name = "MCP_STATE"
387# class_name = "McpState"
388"#,
389 name = args.name,
390 );
391
392 let lib_rs = format!(
394 r#"//! {description}
395
396use turbomcp_wasm::prelude::*;
397use serde::Deserialize;
398use worker::*;
399
400#[derive(Clone)]
401struct {struct_name};
402
403#[derive(Deserialize, schemars::JsonSchema)]
404struct HelloArgs {{
405 /// Name to greet
406 name: String,
407}}
408
409#[server(name = "{name}", version = "0.1.0")]
410impl {struct_name} {{
411 /// Say hello to someone
412 #[tool("Say hello to someone")]
413 async fn hello(&self, args: HelloArgs) -> String {{
414 format!("Hello, {{}}!", args.name)
415 }}
416
417 /// Get server status
418 #[tool("Check server health")]
419 async fn status(&self) -> String {{
420 "OK".to_string()
421 }}
422}}
423
424#[event(fetch)]
425async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {{
426 console_error_panic_hook::set_once();
427
428 let server = {struct_name};
429 let mcp = server.into_mcp_server();
430
431 mcp.handle(req).await
432}}
433"#,
434 description = description,
435 struct_name = to_struct_name(&args.name),
436 name = args.name,
437 );
438
439 write_file(output_dir, "Cargo.toml", &cargo_toml)?;
441 write_file(output_dir, "wrangler.toml", &wrangler_toml)?;
442 fs::create_dir_all(output_dir.join("src"))?;
443 write_file(&output_dir.join("src"), "lib.rs", &lib_rs)?;
444
445 write_file(output_dir, ".gitignore", "/target\n/build\n/node_modules\n")?;
447
448 Ok(())
449}
450
451fn generate_cloudflare_workers_oauth(args: &NewArgs, output_dir: &Path) -> CliResult<()> {
453 let description = args
454 .description
455 .as_deref()
456 .unwrap_or("An MCP server for Cloudflare Workers with OAuth 2.1");
457
458 let cargo_toml = format!(
460 r#"[package]
461name = "{name}"
462version = "0.1.0"
463edition = "2024"
464description = "{description}"
465{author}
466
467[lib]
468crate-type = ["cdylib"]
469
470[dependencies]
471turbomcp-wasm = {{ version = "3.0", features = ["macros", "streamable", "auth"] }}
472worker = "0.5"
473serde = {{ version = "1", features = ["derive"] }}
474serde_json = "1"
475schemars = "0.8"
476wasm-bindgen = "0.2"
477wasm-bindgen-futures = "0.4"
478
479[profile.release]
480opt-level = "s"
481lto = true
482"#,
483 name = args.name,
484 description = description,
485 author = args
486 .author
487 .as_ref()
488 .map(|a| format!("authors = [\"{}\"]", a))
489 .unwrap_or_default(),
490 );
491
492 let wrangler_toml = format!(
494 r#"name = "{name}"
495main = "build/worker/shim.mjs"
496compatibility_date = "2024-01-01"
497
498[build]
499command = "cargo install -q worker-build && worker-build --release"
500
501# OAuth token storage
502[[kv_namespaces]]
503binding = "OAUTH_TOKENS"
504id = "your-kv-namespace-id"
505
506# Secrets (set via wrangler secret put)
507# JWT_SECRET - Secret for signing JWT tokens
508# OAUTH_CLIENT_SECRET - OAuth client secret
509"#,
510 name = args.name,
511 );
512
513 let lib_rs = format!(
515 r#"//! {description}
516
517use turbomcp_wasm::prelude::*;
518use turbomcp_wasm::wasm_server::{{WithAuth, AuthExt}};
519use serde::Deserialize;
520use worker::*;
521use std::sync::Arc;
522
523#[derive(Clone)]
524struct {struct_name};
525
526#[derive(Deserialize, schemars::JsonSchema)]
527struct HelloArgs {{
528 /// Name to greet
529 name: String,
530}}
531
532#[server(name = "{name}", version = "0.1.0")]
533impl {struct_name} {{
534 /// Say hello to someone (requires authentication)
535 #[tool("Say hello to someone")]
536 async fn hello(&self, ctx: Arc<RequestContext>, args: HelloArgs) -> Result<String, ToolError> {{
537 // Check authentication
538 if !ctx.is_authenticated() {{
539 return Err(ToolError::new("Authentication required"));
540 }}
541
542 let user = ctx.user_id().unwrap_or("unknown");
543 Ok(format!("Hello, {{}}! (authenticated as {{}})", args.name, user))
544 }}
545
546 /// Get server status (public)
547 #[tool("Check server health")]
548 async fn status(&self) -> String {{
549 "OK".to_string()
550 }}
551}}
552
553#[event(fetch)]
554async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {{
555 console_error_panic_hook::set_once();
556
557 let server = {struct_name};
558 let mcp = server
559 .into_mcp_server()
560 .with_jwt_auth(env.secret("JWT_SECRET")?.to_string());
561
562 mcp.handle(req).await
563}}
564"#,
565 description = description,
566 struct_name = to_struct_name(&args.name),
567 name = args.name,
568 );
569
570 write_file(output_dir, "Cargo.toml", &cargo_toml)?;
572 write_file(output_dir, "wrangler.toml", &wrangler_toml)?;
573 fs::create_dir_all(output_dir.join("src"))?;
574 write_file(&output_dir.join("src"), "lib.rs", &lib_rs)?;
575
576 write_file(output_dir, ".gitignore", "/target\n/build\n/node_modules\n")?;
578
579 Ok(())
580}
581
582fn generate_cloudflare_workers_do(args: &NewArgs, output_dir: &Path) -> CliResult<()> {
584 let description = args
585 .description
586 .as_deref()
587 .unwrap_or("An MCP server for Cloudflare Workers with Durable Objects");
588
589 let cargo_toml = format!(
591 r#"[package]
592name = "{name}"
593version = "0.1.0"
594edition = "2024"
595description = "{description}"
596{author}
597
598[lib]
599crate-type = ["cdylib"]
600
601[dependencies]
602turbomcp-wasm = {{ version = "3.0", features = ["macros", "streamable"] }}
603worker = "0.5"
604serde = {{ version = "1", features = ["derive"] }}
605serde_json = "1"
606schemars = "0.8"
607wasm-bindgen = "0.2"
608wasm-bindgen-futures = "0.4"
609
610[profile.release]
611opt-level = "s"
612lto = true
613"#,
614 name = args.name,
615 description = description,
616 author = args
617 .author
618 .as_ref()
619 .map(|a| format!("authors = [\"{}\"]", a))
620 .unwrap_or_default(),
621 );
622
623 let wrangler_toml = format!(
625 r#"name = "{name}"
626main = "build/worker/shim.mjs"
627compatibility_date = "2024-01-01"
628
629[build]
630command = "cargo install -q worker-build && worker-build --release"
631
632# Durable Objects for session and state management
633[[durable_objects.bindings]]
634name = "MCP_SESSIONS"
635class_name = "McpSession"
636
637[[durable_objects.bindings]]
638name = "MCP_STATE"
639class_name = "McpState"
640
641[[durable_objects.bindings]]
642name = "MCP_RATE_LIMIT"
643class_name = "McpRateLimit"
644
645[[migrations]]
646tag = "v1"
647new_classes = ["McpSession", "McpState", "McpRateLimit"]
648"#,
649 name = args.name,
650 );
651
652 let lib_rs = format!(
654 r#"//! {description}
655
656use turbomcp_wasm::prelude::*;
657use turbomcp_wasm::wasm_server::{{
658 DurableObjectSessionStore,
659 DurableObjectStateStore,
660 DurableObjectRateLimiter,
661 RateLimitConfig,
662 StreamableHandler,
663}};
664use serde::{{Deserialize, Serialize}};
665use worker::*;
666use std::sync::Arc;
667
668#[derive(Clone)]
669struct {struct_name} {{
670 state_store: DurableObjectStateStore,
671}}
672
673#[derive(Deserialize, schemars::JsonSchema)]
674struct HelloArgs {{
675 /// Name to greet
676 name: String,
677}}
678
679#[derive(Deserialize, schemars::JsonSchema)]
680struct SaveArgs {{
681 /// Key to save under
682 key: String,
683 /// Value to save
684 value: String,
685}}
686
687#[derive(Deserialize, schemars::JsonSchema)]
688struct LoadArgs {{
689 /// Key to load
690 key: String,
691}}
692
693#[server(name = "{name}", version = "0.1.0")]
694impl {struct_name} {{
695 /// Say hello to someone
696 #[tool("Say hello to someone")]
697 async fn hello(&self, args: HelloArgs) -> String {{
698 format!("Hello, {{}}!", args.name)
699 }}
700
701 /// Save a value to persistent state
702 #[tool("Save a value to persistent storage")]
703 async fn save(&self, ctx: Arc<RequestContext>, args: SaveArgs) -> Result<String, ToolError> {{
704 let session_id = ctx.session_id().unwrap_or("default");
705
706 self.state_store
707 .set(session_id, &args.key, &args.value)
708 .await
709 .map_err(|e| ToolError::new(format!("Failed to save: {{}}", e)))?;
710
711 Ok(format!("Saved '{{}}' = '{{}}'", args.key, args.value))
712 }}
713
714 /// Load a value from persistent state
715 #[tool("Load a value from persistent storage")]
716 async fn load(&self, ctx: Arc<RequestContext>, args: LoadArgs) -> Result<String, ToolError> {{
717 let session_id = ctx.session_id().unwrap_or("default");
718
719 let value: Option<String> = self.state_store
720 .get(session_id, &args.key)
721 .await
722 .map_err(|e| ToolError::new(format!("Failed to load: {{}}", e)))?;
723
724 match value {{
725 Some(v) => Ok(v),
726 None => Ok(format!("Key '{{}}' not found", args.key)),
727 }}
728 }}
729}}
730
731#[event(fetch)]
732async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {{
733 console_error_panic_hook::set_once();
734
735 // Initialize Durable Object stores
736 let session_store = DurableObjectSessionStore::from_env(&env, "MCP_SESSIONS")?;
737 let state_store = DurableObjectStateStore::from_env(&env, "MCP_STATE")?;
738 let rate_limiter = DurableObjectRateLimiter::from_env(&env, "MCP_RATE_LIMIT")?
739 .with_config(RateLimitConfig::per_minute(100));
740
741 let server = {struct_name} {{ state_store }};
742 let mcp = server.into_mcp_server();
743
744 // Use Streamable HTTP with session persistence
745 let handler = StreamableHandler::new(mcp)
746 .with_session_store(session_store);
747
748 handler.handle(req).await
749}}
750
751// Durable Object implementations (minimal stubs - expand as needed)
752// See turbomcp_wasm::wasm_server::durable_objects for protocol documentation
753
754#[durable_object]
755pub struct McpSession {{
756 state: State,
757 #[allow(dead_code)]
758 env: Env,
759}}
760
761#[durable_object]
762impl DurableObject for McpSession {{
763 fn new(state: State, env: Env) -> Self {{
764 Self {{ state, env }}
765 }}
766
767 async fn fetch(&mut self, req: Request) -> Result<Response> {{
768 // Handle session storage requests
769 // See DurableObjectSessionStore protocol documentation
770 Response::ok("{{}}")
771 }}
772}}
773
774#[durable_object]
775pub struct McpState {{
776 state: State,
777 #[allow(dead_code)]
778 env: Env,
779}}
780
781#[durable_object]
782impl DurableObject for McpState {{
783 fn new(state: State, env: Env) -> Self {{
784 Self {{ state, env }}
785 }}
786
787 async fn fetch(&mut self, req: Request) -> Result<Response> {{
788 // Handle state storage requests
789 // See DurableObjectStateStore protocol documentation
790 Response::ok("{{}}")
791 }}
792}}
793
794#[durable_object]
795pub struct McpRateLimit {{
796 state: State,
797 #[allow(dead_code)]
798 env: Env,
799}}
800
801#[durable_object]
802impl DurableObject for McpRateLimit {{
803 fn new(state: State, env: Env) -> Self {{
804 Self {{ state, env }}
805 }}
806
807 async fn fetch(&mut self, req: Request) -> Result<Response> {{
808 // Handle rate limiting requests
809 // See DurableObjectRateLimiter protocol documentation
810 Response::ok("{{}}")
811 }}
812}}
813"#,
814 description = description,
815 struct_name = to_struct_name(&args.name),
816 name = args.name,
817 );
818
819 write_file(output_dir, "Cargo.toml", &cargo_toml)?;
821 write_file(output_dir, "wrangler.toml", &wrangler_toml)?;
822 fs::create_dir_all(output_dir.join("src"))?;
823 write_file(&output_dir.join("src"), "lib.rs", &lib_rs)?;
824
825 write_file(output_dir, ".gitignore", "/target\n/build\n/node_modules\n")?;
827
828 Ok(())
829}
830
831fn init_git(output_dir: &Path) -> CliResult<()> {
833 let status = Command::new("git")
834 .arg("init")
835 .current_dir(output_dir)
836 .stdout(std::process::Stdio::null())
837 .stderr(std::process::Stdio::null())
838 .status();
839
840 match status {
841 Ok(s) if s.success() => {
842 println!(" Initialized git repository");
843 Ok(())
844 }
845 _ => {
846 println!(" Warning: Failed to initialize git repository");
847 Ok(()) }
849 }
850}
851
852fn write_file(dir: &Path, name: &str, content: &str) -> CliResult<()> {
854 let path = dir.join(name);
855 fs::write(&path, content)
856 .map_err(|e| CliError::Other(format!("Failed to write {}: {}", path.display(), e)))
857}
858
859fn to_struct_name(name: &str) -> String {
861 let name = name
862 .chars()
863 .map(|c| if c.is_alphanumeric() { c } else { '_' })
864 .collect::<String>();
865
866 let mut result = String::new();
868 let mut capitalize_next = true;
869
870 for c in name.chars() {
871 if c == '_' {
872 capitalize_next = true;
873 } else if capitalize_next {
874 result.push(c.to_ascii_uppercase());
875 capitalize_next = false;
876 } else {
877 result.push(c);
878 }
879 }
880
881 if result.chars().next().is_none_or(|c| c.is_ascii_digit()) {
883 result = format!("Server{}", result);
884 }
885
886 result
887}
888
889#[cfg(test)]
890mod tests {
891 use super::*;
892
893 #[test]
894 fn test_to_struct_name() {
895 assert_eq!(to_struct_name("my-server"), "MyServer");
896 assert_eq!(to_struct_name("hello_world"), "HelloWorld");
897 assert_eq!(to_struct_name("123test"), "Server123test");
898 assert_eq!(to_struct_name("simple"), "Simple");
899 }
900
901 #[test]
902 fn test_template_display() {
903 assert_eq!(ProjectTemplate::Minimal.to_string(), "minimal");
904 assert_eq!(
905 ProjectTemplate::CloudflareWorkers.to_string(),
906 "cloudflare-workers"
907 );
908 }
909}