Clients should use STDIO (vibe-kanban cfddf3b2)

Instead of connecting via SSE, we should allow clients to connect via STDIO. This is a necessary change to solve some issues
This commit is contained in:
Louis Knight-Webb
2025-10-30 14:18:43 +00:00
parent c6c68b5916
commit b93283b0f0
6 changed files with 848 additions and 15 deletions

109
src/bin/stdio_proxy.rs Normal file
View File

@@ -0,0 +1,109 @@
use anyhow::Result;
use rmcp::service::RxJsonRpcMessage;
use rmcp::transport::async_rw::AsyncRwTransport;
use rmcp::transport::sse_client::SseClientTransport;
use rmcp::transport::{io::stdio, Transport};
use rmcp::RoleServer;
use serde_json::Value;
use tokio::sync::mpsc;
fn inject_cwd_if_start_tool(
msg: RxJsonRpcMessage<RoleServer>,
client_cwd: Option<String>,
) -> RxJsonRpcMessage<RoleServer> {
let client_cwd = match client_cwd {
Some(cwd) => cwd,
None => return msg,
};
let mut msg_value = match serde_json::to_value(&msg) {
Ok(v) => v,
Err(_) => return msg,
};
if let Some(params) = msg_value.get_mut("params").and_then(|p| p.as_object_mut()) {
if let Some(name) = params.get("name").and_then(|n| n.as_str()) {
if name == "start" {
if let Some(arguments) = params.get_mut("arguments").and_then(|a| a.as_object_mut()) {
if !arguments.contains_key("cwd") {
arguments.insert("cwd".to_string(), Value::String(client_cwd));
return serde_json::from_value(msg_value).unwrap_or(msg);
}
}
}
}
}
msg
}
#[tokio::main]
async fn main() -> Result<()> {
let daemon_url = std::env::var("MCP_DAEMON_URL")
.unwrap_or_else(|_| "http://127.0.0.1:3009/sse".to_string());
let client_cwd = std::env::current_dir()
.ok()
.and_then(|p| p.to_str().map(String::from));
if let Some(ref cwd) = client_cwd {
eprintln!("STDIO proxy starting in CWD: {}", cwd);
}
eprintln!("STDIO proxy connecting to daemon at {}", daemon_url);
let (stdin, stdout) = stdio();
let mut stdio_transport = AsyncRwTransport::<RoleServer, _, _>::new_server(stdin, stdout);
let mut sse_transport = SseClientTransport::start(daemon_url.as_str()).await?;
let (req_tx, mut req_rx) = mpsc::unbounded_channel();
let (resp_tx, mut resp_rx) = mpsc::unbounded_channel();
let stdio_task = tokio::spawn(async move {
loop {
tokio::select! {
msg = stdio_transport.receive() => {
match msg {
Some(c2s) => {
let modified_msg = inject_cwd_if_start_tool(c2s, client_cwd.clone());
let _ = req_tx.send(modified_msg);
}
None => break,
}
}
Some(s2c) = resp_rx.recv() => {
if let Err(e) = stdio_transport.send(s2c).await {
eprintln!("STDIO send error: {}", e);
break;
}
}
}
}
});
let sse_task = tokio::spawn(async move {
loop {
tokio::select! {
msg = sse_transport.receive() => {
match msg {
Some(s2c) => { let _ = resp_tx.send(s2c); }
None => break,
}
}
Some(c2s) = req_rx.recv() => {
if let Err(e) = sse_transport.send(c2s).await {
eprintln!("SSE send error: {}", e);
break;
}
}
}
}
});
tokio::select! {
_ = stdio_task => eprintln!("STDIO task ended"),
_ = sse_task => eprintln!("SSE task ended"),
_ = tokio::signal::ctrl_c() => eprintln!("Interrupted"),
}
Ok(())
}

View File

@@ -127,7 +127,7 @@ impl Manager {
});
}
pub async fn start(&self, command: String) -> serde_json::Value {
pub async fn start(&self, command: String, cwd: Option<String>) -> serde_json::Value {
let (session_key, port) = {
let mut guard = match self.inner.lock() {
Ok(g) => g,
@@ -161,6 +161,16 @@ impl Manager {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
if let Some(ref cwd_path) = cwd {
let path = std::path::Path::new(cwd_path);
if !path.is_dir() {
let mut guard = self.inner.lock().unwrap();
guard.port_allocator.free(port);
return json!({"error": format!("Invalid cwd: {}", cwd_path)});
}
cmd.current_dir(path);
}
let child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {

View File

@@ -14,6 +14,7 @@ use std::sync::Arc;
#[derive(Deserialize, JsonSchema, Clone)]
struct StartRequest {
command: String,
cwd: Option<String>,
}
#[derive(Deserialize, JsonSchema, Clone)]
@@ -50,7 +51,7 @@ impl DevManagerService {
impl DevManagerService {
#[tool(description = "Start a development server. Returns auto-generated session key, port number and status.")]
async fn start(&self, Parameters(req): Parameters<StartRequest>) -> Result<CallToolResult, ErrorData> {
let result = self.manager.start(req.command).await;
let result = self.manager.start(req.command, req.cwd).await;
Ok(CallToolResult::success(vec![Content::text(result.to_string())]))
}