1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/*
* bevy-fairy - Proper SDK Implementation
* Copyright (C) 2025 ladvien
*
* Licensed under either of MIT (LICENSE-MIT) or Apache-2.0 (LICENSE-APACHE), at your option.
*
* Relicensed from GPL-3.0 when this crate was adopted into Ladvien/foundation_vs_slop: a GPL
* crate in the Bevy ecosystem cannot be adopted, and being adoptable is why it is published.
*/
use rmcp::serve_server;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};
use crate::server::brp_client::BrpClient;
use crate::server::config::Config;
use crate::server::error::Result;
use crate::server::mcp_tools::BevyDebuggerTools;
/// Proper MCP server implementation using the official SDK
pub struct McpServerV2 {
config: Config,
brp_client: Arc<RwLock<BrpClient>>,
tools: Arc<BevyDebuggerTools>,
}
impl McpServerV2 {
pub fn new(config: Config, brp_client: Arc<RwLock<BrpClient>>) -> Result<Self> {
let tools = Arc::new(BevyDebuggerTools::new(brp_client.clone()));
Ok(Self {
config,
brp_client,
tools,
})
}
/// Run the server in stdio mode for Claude Code
pub async fn run_stdio(self) -> Result<()> {
info!("Starting MCP server in stdio mode for Claude Code integration");
// Register the core BRP handler. A missing game is not fatal: the heartbeat below reports it.
{
let client = self.brp_client.read().await;
if let Err(e) = client.init().await {
warn!("bevy-fairy: BRP client init failed: {e}; tools will retry");
}
}
// Keep the connection state current in the background. Each probe is one HTTP request made under
// the read lock, so tool calls never wait behind a retry sleep. Log only when the state changes.
let brp_client = self.brp_client.clone();
tokio::spawn(async move {
let mut reachable: Option<bool> = None;
loop {
let (url, probe) = {
let client = brp_client.read().await;
(client.url().to_string(), client.probe().await)
};
match probe {
Ok(()) if reachable != Some(true) => {
info!("bevy-fairy: game answering at {url}");
reachable = Some(true);
}
Ok(()) => {}
Err(e) if reachable != Some(false) => {
warn!("bevy-fairy: no game answering at {url} yet; tools will retry ({e})");
reachable = Some(false);
}
Err(_) => {}
}
tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
}
});
// Setup signal handlers for graceful shutdown
let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel(1);
// Handle SIGTERM and SIGINT
tokio::spawn(async move {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(sig) => sig,
Err(e) => {
error!("Failed to setup SIGTERM handler: {e}");
// Park, do not return: returning drops shutdown_tx and the main loop would exit at once.
std::future::pending::<()>().await;
return;
}
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(sig) => sig,
Err(e) => {
error!("Failed to setup SIGINT handler: {e}");
std::future::pending::<()>().await;
return;
}
};
tokio::select! {
_ = sigterm.recv() => {
info!("Received SIGTERM, shutting down gracefully");
}
_ = sigint.recv() => {
info!("Received SIGINT, shutting down gracefully");
}
}
}
#[cfg(not(unix))]
{
if let Err(e) = tokio::signal::ctrl_c().await {
error!("Failed to setup Ctrl-C handler: {e}");
std::future::pending::<()>().await;
}
info!("Received Ctrl-C, shutting down gracefully");
}
let _ = shutdown_tx.send(()).await;
});
info!("MCP stdio transport starting - ready for Claude Code connection");
// Create stdio transport
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
// Serve the tool router over stdio. Stdio ends when the client disconnects.
tokio::select! {
result = serve_server((*self.tools).clone(), (stdin, stdout)) => {
match result {
// **`serve_server` resolves once the service is RUNNING, not once it is done.** It
// hands back a `RunningService` handle owning the task that reads the transport.
// Returning here dropped that handle, which cancelled the task the moment the
// client finished initializing — so `initialize` was answered and the very next
// request was not. Claude Code showed that as
// `Connected · tools fetch failed — MCP error -32000: Connection closed`,
// which reads like a transport fault and is really a dropped handle.
//
// `waiting()` awaits the service's own quit reason, which is what "the server ran"
// actually means.
Ok(service) => match service.waiting().await {
Ok(reason) => {
info!("MCP stdio server finished: {reason:?}");
Ok(())
}
Err(e) => {
error!("MCP stdio server task failed to join: {}", e);
Err(crate::server::error::Error::DebugError(format!(
"MCP stdio server task failed to join: {e}"
)))
}
},
Err(e) => {
error!("MCP stdio server error: {}", e);
Err(crate::server::error::Error::DebugError(format!("MCP stdio server failed: {}", e)))
}
}
}
_ = shutdown_rx.recv() => {
info!("Graceful shutdown requested");
Ok(())
}
}
}
/// Run the server in TCP mode for background operation
pub async fn run_tcp(self) -> Result<()> {
info!("Starting MCP server in TCP mode on port {}", self.config.mcp_port);
// For now, TCP mode is not implemented
// You can use stdio mode instead
error!("TCP mode not implemented, use stdio mode");
Err(crate::server::error::Error::DebugError("TCP mode not implemented".to_string()))
}
}
// McpServerV2 acts as a coordinator - the actual MCP handling is done by BevyDebuggerTools
// No ServerHandler implementation needed here since tools handle the MCP protocol directly