Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ toml = { version = "0.8" }
serde_json = "1"
dirs = "6"
chrono = "0.4"
rmcp = { version = "1.8.0", features = ["server", "transport-io"] }
rmcp = { version = "1.8.0", features = ["server", "transport-io", "transport-streamable-http-server"] }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "io-util", "io-std", "sync", "net", "time"] }
ureq = { version = "2", features = ["json"] }
sha2 = "0.10"
Expand Down
27 changes: 25 additions & 2 deletions src/cli/mcp.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
use clap::Args;

/// Run the agentflare MCP server over stdio (for editor/agent MCP clients).
/// Run the agentflare MCP server (stdio by default; `--http` serves many
/// concurrent agent sessions from one persistent process over Streamable HTTP).
#[derive(Args)]
pub struct McpArgs;
pub struct McpArgs {
/// Serve over Streamable HTTP/SSE instead of stdio, so one agentflare
/// process backs many concurrent agent sessions instead of one per session.
#[arg(long)]
pub http: bool,
/// TCP port for `--http`. Default 35274 ("FLARE" shifted one up from the
/// dashboard's 35273); 0 = auto-assign.
#[arg(long, default_value = "35274")]
pub port: u16,
/// Interface to bind for `--http`. Loopback only for now: rmcp's default
/// Host allowlist rejects remote clients, and there is no auth yet.
#[arg(long, default_value = "127.0.0.1")]
pub host: String,
}

impl McpArgs {
pub fn run(self) {
if self.http {
let runtime = tokio::runtime::Runtime::new()
.expect("failed to build tokio runtime for mcp server");
if let Err(e) = runtime.block_on(crate::mcp_server::run_http(&self.host, self.port)) {
crate::ui::error(&format!("agentflare mcp: {e}"));
std::process::exit(1);
}
return;
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
Expand Down
35 changes: 35 additions & 0 deletions src/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,41 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}

/// Serve the MCP server over Streamable HTTP/SSE so one persistent agentflare
/// process backs many concurrent agent sessions, instead of one cold-started
/// OS process per connecting agent (the stdio default).
pub async fn run_http(
host: &str,
port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use rmcp::transport::streamable_http_server::{
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
};

// One server instance shared across every session: the point of HTTP mode
// is that a single process (and its lazily-opened skills/gateway registries
// and backend/store DB connections) backs many concurrent agent sessions,
// instead of cold-starting one per connection. The factory clones the Arc,
// so each rmcp session routes into the same `AgentflareMcp`.
let shared = std::sync::Arc::new(AgentflareMcp::from_env());

let service: StreamableHttpService<std::sync::Arc<AgentflareMcp>, LocalSessionManager> =
StreamableHttpService::new(
{
let shared = shared.clone();
move || Ok(shared.clone())
},
std::sync::Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
);
let app = axum::Router::new().nest_service("/mcp", service);
let listener = tokio::net::TcpListener::bind((host, port)).await?;
let addr = listener.local_addr()?;
eprintln!("agentflare MCP listening on http://{addr}/mcp (streamable HTTP)");
axum::serve(listener, app).await?;
Ok(())
}

#[cfg(test)]
mod optimize_instructions_tests {
use super::build_optimize_instructions;
Expand Down
Loading