1use axum::{
54 extract::{Path, State},
55 http::StatusCode,
56 response::{IntoResponse, Response},
57 routing::{get, post},
58 Json, Router,
59};
60use rmcp::transport::streamable_http_server::{
61 session::local::LocalSessionManager, StreamableHttpService,
62};
63use serde::Serialize;
64use std::sync::Arc;
65use tower_http::cors::{Any, CorsLayer};
66
67use crate::agent_script::{load_agent_definitions, LuaAgentAdapter};
68use crate::agents::{AgentInfo, AgentRegistry};
69use crate::config::Config;
70use crate::mcp::McpBridge;
71use crate::registry::RegistryManager;
72use crate::tool_script::{load_tool_definitions, validate_params, LuaToolAdapter, ToolInfo};
73use crate::traits::{ToolContext, ToolRegistry};
74use crate::workspace::{RouterError, ServerMode, WorkspaceRouter};
75
76#[derive(Clone)]
78struct AppState {
79 router: Arc<WorkspaceRouter>,
82 mode: ServerMode,
84 tools: Arc<ToolRegistry>,
86 agents: Arc<AgentRegistry>,
88}
89
90type ExtState = (Arc<ToolRegistry>, Arc<AgentRegistry>);
92
93pub async fn run_server(config: &Config) -> anyhow::Result<()> {
110 run_server_with_extensions(
111 config,
112 Arc::new(ToolRegistry::new()),
113 Arc::new(AgentRegistry::new()),
114 )
115 .await
116}
117
118pub async fn run_server_with_extensions(
144 config: &Config,
145 extra_tools: Arc<ToolRegistry>,
146 extra_agents: Arc<AgentRegistry>,
147) -> anyhow::Result<()> {
148 let bind_addr = config.server.bind.clone();
149 let config = Arc::new(config.clone());
150
151 let mut tool_registry = ToolRegistry::with_builtins();
153
154 let lua_defs = load_tool_definitions(&config)?;
156 let configured_tool_names: Vec<String> = lua_defs.iter().map(|d| d.name.clone()).collect();
157 for def in lua_defs {
158 tool_registry.register(Box::new(LuaToolAdapter::new(def, config.clone())));
159 }
160
161 let reg_mgr = RegistryManager::from_config(&config);
163 for ext in reg_mgr.list_tools() {
164 if configured_tool_names.iter().any(|n| n == &ext.name) {
165 continue;
166 }
167 if !ext.script_path.exists() {
168 continue;
169 }
170 let tool_cfg = crate::config::ScriptToolConfig {
171 path: ext.script_path.clone(),
172 timeout: 30,
173 extra: toml::Table::new(),
174 };
175 match crate::tool_script::load_single_tool(&ext.name, &tool_cfg) {
176 Ok(def) => {
177 tool_registry.register(Box::new(LuaToolAdapter::new(def, config.clone())));
178 }
179 Err(e) => {
180 eprintln!(
181 "Warning: failed to load registry tool '{}': {}",
182 ext.name, e
183 );
184 }
185 }
186 }
187
188 let tool_count = tool_registry.len() + extra_tools.len();
190 if tool_count > 3 {
191 println!("Registered {} tools:", tool_count);
192 for t in tool_registry.tools() {
193 let tag = if t.is_builtin() { "builtin" } else { "lua" };
194 println!(" POST /tools/{} — {} ({})", t.name(), t.description(), tag);
195 }
196 for t in extra_tools.tools() {
197 println!(" POST /tools/{} — {} (rust)", t.name(), t.description());
198 }
199 }
200
201 let mut agent_registry = AgentRegistry::from_config(&config)?;
203
204 let lua_agents = load_agent_definitions(&config)?;
206 let configured_agent_names: Vec<String> = lua_agents.iter().map(|d| d.name.clone()).collect();
207 for def in lua_agents {
208 agent_registry.register(Box::new(LuaAgentAdapter::new(def, config.clone())));
209 }
210
211 for ext in reg_mgr.list_agents() {
213 if configured_agent_names.iter().any(|n| n == &ext.name) {
214 continue;
215 }
216 if !ext.script_path.exists() {
217 continue;
218 }
219 if ext.script_path.extension().is_some_and(|e| e == "lua") {
220 let agent_cfg = crate::config::ScriptAgentConfig {
221 path: ext.script_path.clone(),
222 timeout: 30,
223 extra: toml::Table::new(),
224 };
225 match crate::agent_script::load_single_agent(&ext.name, &agent_cfg) {
226 Ok(def) => {
227 agent_registry.register(Box::new(LuaAgentAdapter::new(def, config.clone())));
228 }
229 Err(e) => {
230 eprintln!(
231 "Warning: failed to load registry agent '{}': {}",
232 ext.name, e
233 );
234 }
235 }
236 }
237 }
238
239 let agent_count = agent_registry.len() + extra_agents.len();
240 if agent_count > 0 {
241 println!("Registered {} agents:", agent_count);
242 for a in agent_registry.agents() {
243 println!(
244 " POST /agents/{}/prompt — {} ({})",
245 a.name(),
246 a.description(),
247 a.source()
248 );
249 }
250 for a in extra_agents.agents() {
251 println!(
252 " POST /agents/{}/prompt — {} ({})",
253 a.name(),
254 a.description(),
255 a.source()
256 );
257 }
258 }
259
260 let tools = Arc::new(tool_registry);
261 let agents = Arc::new(agent_registry);
262
263 let router = Arc::new(WorkspaceRouter::single(config.clone()));
266
267 serve_router(
268 router,
269 ServerMode::Compat,
270 bind_addr,
271 false,
272 tools,
273 agents,
274 extra_tools,
275 extra_agents,
276 )
277 .await
278}
279
280pub async fn run_server_multi(
288 router: Arc<WorkspaceRouter>,
289 bind: String,
290 allow_remote: bool,
291) -> anyhow::Result<()> {
292 let tools = Arc::new(ToolRegistry::with_builtins_multi());
293 let agents = Arc::new(AgentRegistry::new());
294 let extra_tools = Arc::new(ToolRegistry::new());
295 let extra_agents = Arc::new(AgentRegistry::new());
296
297 println!("Multi-workspace MCP mode. Registered workspaces:");
298 for rt in router.list() {
299 let default = if Some(rt.id.as_str()) == router.default_id() {
300 " (default)"
301 } else {
302 ""
303 };
304 let health = if rt.health.is_ok() {
305 "ok"
306 } else {
307 "unavailable"
308 };
309 println!(
310 " {}{} — enabled={} health={} resolution={}",
311 rt.id,
312 default,
313 rt.enabled,
314 health,
315 rt.resolution.as_str()
316 );
317 }
318
319 serve_router(
320 router,
321 ServerMode::Multi,
322 bind,
323 allow_remote,
324 tools,
325 agents,
326 extra_tools,
327 extra_agents,
328 )
329 .await
330}
331
332fn is_loopback_bind(bind: &str) -> bool {
334 let host = bind.rsplit_once(':').map(|(h, _)| h).unwrap_or(bind);
335 let host = host.trim_start_matches('[').trim_end_matches(']');
336 if host.eq_ignore_ascii_case("localhost") {
337 return true;
338 }
339 host.parse::<std::net::IpAddr>()
340 .map(|ip| ip.is_loopback())
341 .unwrap_or(false)
342}
343
344#[allow(clippy::too_many_arguments)]
346async fn serve_router(
347 router: Arc<WorkspaceRouter>,
348 mode: ServerMode,
349 bind_addr: String,
350 allow_remote: bool,
351 tools: Arc<ToolRegistry>,
352 agents: Arc<AgentRegistry>,
353 extra_tools: Arc<ToolRegistry>,
354 extra_agents: Arc<AgentRegistry>,
355) -> anyhow::Result<()> {
356 if !is_loopback_bind(&bind_addr) {
360 if mode == ServerMode::Multi && !allow_remote {
361 anyhow::bail!(
362 "refusing to bind multi-workspace server to non-loopback address '{bind_addr}': \
363 this exposes every registered workspace to other hosts. Bind to 127.0.0.1, or \
364 pass --allow-remote to override."
365 );
366 }
367 eprintln!(
368 "warning: binding to non-loopback address '{bind_addr}'. The MCP server has \
369 permissive CORS and no authentication; only bind to addresses reachable by \
370 trusted clients."
371 );
372 }
373
374 let state = AppState {
375 router: router.clone(),
376 mode,
377 tools: tools.clone(),
378 agents: agents.clone(),
379 };
380
381 let mcp_tools = tools.clone();
383 let mcp_extra = extra_tools.clone();
384 let mcp_agents = agents.clone();
385 let mcp_extra_agents = extra_agents.clone();
386 let mcp_router = router.clone();
387
388 let extra_state = (extra_tools.clone(), extra_agents);
389 let mcp_service = StreamableHttpService::new(
390 move || {
391 Ok(McpBridge::new(
392 mcp_router.clone(),
393 mode,
394 mcp_tools.clone(),
395 mcp_extra.clone(),
396 mcp_agents.clone(),
397 mcp_extra_agents.clone(),
398 ))
399 },
400 Arc::new(LocalSessionManager::default()),
401 Default::default(),
402 );
403
404 let cors = CorsLayer::new()
405 .allow_origin(Any)
406 .allow_methods(Any)
407 .allow_headers(Any);
408
409 let app = Router::new()
410 .route("/tools/list", get(handle_list_tools))
411 .route("/tools/{name}", post(handle_tool_call))
412 .route("/agents/list", get(handle_list_agents))
413 .route("/agents/{name}/prompt", post(handle_resolve_agent))
414 .route("/health", get(handle_health))
415 .with_state((state, extra_state))
416 .nest_service("/mcp", mcp_service)
417 .layer(cors);
418
419 println!("MCP server listening on http://{}", bind_addr);
420 println!(" MCP endpoint: http://{}/mcp", bind_addr);
421
422 let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
423 axum::serve(listener, app).await?;
424
425 Ok(())
426}
427
428#[derive(Serialize)]
432struct ErrorBody {
433 error: ErrorDetail,
434}
435
436#[derive(Serialize)]
438struct ErrorDetail {
439 code: String,
441 message: String,
443}
444
445struct AppError {
447 status: StatusCode,
448 code: String,
449 message: String,
450}
451
452impl IntoResponse for AppError {
453 fn into_response(self) -> Response {
454 let body = ErrorBody {
455 error: ErrorDetail {
456 code: self.code,
457 message: self.message,
458 },
459 };
460 (self.status, Json(body)).into_response()
461 }
462}
463
464fn bad_request(message: impl Into<String>) -> AppError {
466 AppError {
467 status: StatusCode::BAD_REQUEST,
468 code: "bad_request".to_string(),
469 message: message.into(),
470 }
471}
472
473fn not_found(message: impl Into<String>) -> AppError {
475 AppError {
476 status: StatusCode::NOT_FOUND,
477 code: "not_found".to_string(),
478 message: message.into(),
479 }
480}
481
482fn timeout_error(message: impl Into<String>) -> AppError {
484 AppError {
485 status: StatusCode::REQUEST_TIMEOUT,
486 code: "timeout".to_string(),
487 message: message.into(),
488 }
489}
490
491fn tool_error(message: impl Into<String>) -> AppError {
493 AppError {
494 status: StatusCode::INTERNAL_SERVER_ERROR,
495 code: "tool_error".to_string(),
496 message: message.into(),
497 }
498}
499
500fn classify_tool_error(tool_name: &str, err: anyhow::Error) -> AppError {
505 if let Some(re) = err.downcast_ref::<RouterError>() {
507 let status = match re {
508 RouterError::UnknownWorkspace(_) => StatusCode::NOT_FOUND,
509 RouterError::WorkspaceUnavailable { .. } => StatusCode::SERVICE_UNAVAILABLE,
510 RouterError::WorkspaceTimeout { .. } => StatusCode::SERVICE_UNAVAILABLE,
511 _ => StatusCode::BAD_REQUEST,
512 };
513 return AppError {
514 status,
515 code: re.code().to_string(),
516 message: format!("{tool_name}: {re}"),
517 };
518 }
519
520 let msg = err.to_string();
521
522 if msg.contains("not found") {
523 not_found(format!("{}: {}", tool_name, msg))
524 } else if msg.contains("must not be empty")
525 || msg.contains("embeddings")
526 || msg.contains("disabled")
527 || msg.contains("invalid")
528 {
529 let mut e = bad_request(format!("{}: {}", tool_name, msg));
531 if msg.contains("embeddings") || msg.contains("disabled") {
533 e.code = "embeddings_disabled".to_string();
534 }
535 e
536 } else if msg.contains("timed out") {
537 timeout_error(format!("{}: {}", tool_name, msg))
538 } else {
539 tool_error(format!("{}: {}", tool_name, msg))
540 }
541}
542
543#[derive(Serialize)]
547struct HealthResponse {
548 status: String,
550 version: String,
552}
553
554async fn handle_health() -> Json<HealthResponse> {
559 Json(HealthResponse {
560 status: "ok".to_string(),
561 version: env!("CARGO_PKG_VERSION").to_string(),
562 })
563}
564
565#[derive(Serialize)]
569struct ToolListResponse {
570 tools: Vec<ToolInfo>,
572}
573
574async fn handle_list_tools(
580 State((state, (extra_tools, _extra_agents))): State<(AppState, ExtState)>,
581) -> Json<ToolListResponse> {
582 let mut tools: Vec<ToolInfo> = state
583 .tools
584 .tools()
585 .iter()
586 .map(|t| ToolInfo {
587 name: t.name().to_string(),
588 description: t.description().to_string(),
589 builtin: t.is_builtin(),
590 parameters: t.parameters_schema(),
591 })
592 .collect();
593
594 for t in extra_tools.tools() {
596 tools.push(ToolInfo {
597 name: t.name().to_string(),
598 description: t.description().to_string(),
599 builtin: false,
600 parameters: t.parameters_schema(),
601 });
602 }
603
604 Json(ToolListResponse { tools })
605}
606
607async fn handle_tool_call(
618 State((state, (extra_tools, _extra_agents))): State<(AppState, ExtState)>,
619 Path(name): Path<String>,
620 Json(params): Json<serde_json::Value>,
621) -> Result<Json<serde_json::Value>, AppError> {
622 let tool = state
624 .tools
625 .find(&name)
626 .or_else(|| extra_tools.find(&name))
627 .ok_or_else(|| not_found(format!("no tool registered with name: {}", name)))?;
628
629 let validated_params = validate_params(&tool.parameters_schema(), ¶ms)
631 .map_err(|e| bad_request(e.to_string()))?;
632
633 let ctx = ToolContext::routed(state.router.clone(), state.mode);
635 let result = tool
636 .execute(validated_params, &ctx)
637 .await
638 .map_err(|e| classify_tool_error(&name, e))?;
639
640 Ok(Json(serde_json::json!({ "result": result })))
641}
642
643#[derive(Serialize)]
647struct AgentListResponse {
648 agents: Vec<AgentInfo>,
650}
651
652async fn handle_list_agents(
657 State((state, (_extra_tools, extra_agents))): State<(AppState, ExtState)>,
658) -> Json<AgentListResponse> {
659 let mut agents: Vec<AgentInfo> = state
660 .agents
661 .agents()
662 .iter()
663 .map(|a| AgentInfo {
664 name: a.name().to_string(),
665 description: a.description().to_string(),
666 tools: a.tools(),
667 source: a.source().to_string(),
668 arguments: a.arguments(),
669 })
670 .collect();
671
672 for a in extra_agents.agents() {
674 agents.push(AgentInfo {
675 name: a.name().to_string(),
676 description: a.description().to_string(),
677 tools: a.tools(),
678 source: a.source().to_string(),
679 arguments: a.arguments(),
680 });
681 }
682
683 Json(AgentListResponse { agents })
684}
685
686async fn handle_resolve_agent(
697 State((state, (_extra_tools, extra_agents))): State<(AppState, ExtState)>,
698 Path(name): Path<String>,
699 Json(args): Json<serde_json::Value>,
700) -> Result<Json<serde_json::Value>, AppError> {
701 let agent = state
702 .agents
703 .find(&name)
704 .or_else(|| extra_agents.find(&name))
705 .ok_or_else(|| not_found(format!("no agent registered with name: {}", name)))?;
706
707 let ctx = ToolContext::routed(state.router.clone(), state.mode);
708 let prompt = agent
709 .resolve(args, &ctx)
710 .await
711 .map_err(|e| tool_error(format!("agent '{}': {}", name, e)))?;
712
713 Ok(Json(serde_json::to_value(prompt).map_err(|e| {
714 tool_error(format!("failed to serialize agent prompt: {}", e))
715 })?))
716}