mirror of
https://github.com/BloopAI/dev-manager-mcp.git
synced 2026-08-23 11:58:33 +00:00
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:
109
src/bin/stdio_proxy.rs
Normal file
109
src/bin/stdio_proxy.rs
Normal 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(())
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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())]))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user