fix(web): démarre le serveur MCP custom CLI via idea-serve au lieu de node — #224
This commit is contained in:
@ -1,5 +1,12 @@
|
||||
use std::process::ExitCode;
|
||||
|
||||
const MCP_SERVER_SUBCOMMAND: &str = "mcp-server";
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
if args.next().is_some_and(|a| a == *MCP_SERVER_SUBCOMMAND) {
|
||||
let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
|
||||
return backend::mcp_bridge::run_mcp_bridge(rest);
|
||||
}
|
||||
web_server::run_from_args(std::env::args().skip(1).collect())
|
||||
}
|
||||
|
||||
@ -53,10 +53,11 @@ use application::{
|
||||
PairAttemptDecision, PairDeviceInput, RateLimitKey, ReadAgentContextInput,
|
||||
ReadConversationPageInput, ReadIssueAttachmentInput, ReadIssueCarnetInput, ReadIssueInput,
|
||||
ReadMcpToolPermissionsInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
|
||||
RenameDeviceInput, RenameLayoutInput, RenameSprintInput, ReorderSprintsInput,
|
||||
ResizeTerminalInput, ResolveAgentPermissionsInput, ResolveAgentSystemPermissionsInput,
|
||||
ResolveMemoryLinksInput, RevokeDeviceInput, RotateConversationLogInput, SetActiveLayoutInput,
|
||||
StopLiveAgentInput, SyncAgentWithTemplateInput, TouchDeviceInput, UnassignSkillFromAgentInput,
|
||||
ReconcileLayoutsInput, ReconcileLiveStateInput, RenameDeviceInput, RenameLayoutInput,
|
||||
RenameSprintInput, ReorderSprintsInput, ResizeTerminalInput, ResolveAgentPermissionsInput,
|
||||
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
|
||||
RotateConversationLogInput, SetActiveLayoutInput, StopLiveAgentInput,
|
||||
SyncAgentWithTemplateInput, TouchDeviceInput, UnassignSkillFromAgentInput,
|
||||
UnassignTicketFromSprintInput, UnlinkIssuesInput, UpdateAgentContextInput,
|
||||
UpdateAgentEffortInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
|
||||
UpdateAgentSystemPermissionsInput, UpdateIssueCarnetInput, UpdateMemoryInput,
|
||||
@ -506,33 +507,55 @@ fn config_with_effective_listen(mut config: ServerConfig, local_addr: SocketAddr
|
||||
}
|
||||
|
||||
fn resolve_web_root(explicit: Option<PathBuf>) -> Result<PathBuf, String> {
|
||||
if let Some(path) = explicit {
|
||||
return validate_web_root(path, "--web-root");
|
||||
}
|
||||
if let Some(path) = env::var_os("IDEA_WEB_ROOT").map(PathBuf::from) {
|
||||
return validate_web_root(path, "IDEA_WEB_ROOT");
|
||||
}
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push(dir.join("web"));
|
||||
candidates.push(dir.join("frontend").join("dist"));
|
||||
}
|
||||
}
|
||||
if let Ok(cwd) = env::current_dir() {
|
||||
candidates.push(cwd.join("frontend").join("dist"));
|
||||
}
|
||||
|
||||
for candidate in candidates {
|
||||
for (candidate, source) in web_root_candidates(explicit) {
|
||||
if candidate.join("index.html").is_file() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
if matches!(source, "--web-root" | "IDEA_WEB_ROOT") {
|
||||
return validate_web_root(candidate, source);
|
||||
}
|
||||
}
|
||||
|
||||
Err("web assets not found: build frontend/dist or pass --web-root PATH".to_owned())
|
||||
}
|
||||
|
||||
fn web_root_candidates(explicit: Option<PathBuf>) -> Vec<(PathBuf, &'static str)> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(path) = explicit {
|
||||
candidates.push((path, "--web-root"));
|
||||
}
|
||||
if let Some(path) = env::var_os("IDEA_WEB_ROOT").map(PathBuf::from) {
|
||||
candidates.push((path, "IDEA_WEB_ROOT"));
|
||||
}
|
||||
if let Some(appdir) = env::var_os("APPDIR").map(PathBuf::from) {
|
||||
candidates.extend(
|
||||
web_root_candidates_from_appdir(&appdir)
|
||||
.into_iter()
|
||||
.map(|candidate| (candidate, "APPDIR")),
|
||||
);
|
||||
}
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
candidates.push((dir.join("web"), "current_exe"));
|
||||
candidates.push((dir.join("frontend").join("dist"), "current_exe"));
|
||||
if let Some(prefix) = dir.parent() {
|
||||
candidates.push((prefix.join("lib").join("IdeA").join("web"), "current_exe"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(cwd) = env::current_dir() {
|
||||
candidates.push((cwd.join("frontend").join("dist"), "cwd"));
|
||||
}
|
||||
candidates
|
||||
}
|
||||
|
||||
fn web_root_candidates_from_appdir(appdir: &Path) -> Vec<PathBuf> {
|
||||
vec![
|
||||
appdir.join("usr").join("lib").join("IdeA").join("web"),
|
||||
appdir.join("web"),
|
||||
]
|
||||
}
|
||||
|
||||
fn validate_web_root(path: PathBuf, source: &str) -> Result<PathBuf, String> {
|
||||
if path.join("index.html").is_file() {
|
||||
Ok(path)
|
||||
@ -3099,8 +3122,32 @@ async fn invoke_open_project(args: &Value, state: &BackendCore) -> Result<Value,
|
||||
code: "INVALID".to_owned(),
|
||||
message: "open_project requires args.projectId".to_owned(),
|
||||
})?;
|
||||
let project = resolve_project_readonly(project_id, state).await?;
|
||||
serde_json::to_value(ProjectDto::from(project)).map_err(serialization_error)
|
||||
let project = open_project_for_web(project_id, state).await?;
|
||||
serde_json::to_value(project).map_err(serialization_error)
|
||||
}
|
||||
|
||||
async fn open_project_for_web(
|
||||
project_id: &str,
|
||||
state: &BackendCore,
|
||||
) -> Result<ProjectDto, ErrorDto> {
|
||||
let id = parse_project_id(project_id)?;
|
||||
let output = state
|
||||
.open_project
|
||||
.execute(OpenProjectInput { project_id: id })
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
let _ = state
|
||||
.reconcile_layouts
|
||||
.execute(ReconcileLayoutsInput { project_id: id })
|
||||
.await;
|
||||
let _ = state
|
||||
.reconcile_live_state
|
||||
.execute(ReconcileLiveStateInput { project_id: id })
|
||||
.await;
|
||||
let _ = state.reconcile_background_tasks.execute(id).await;
|
||||
state.ensure_orchestrator_watch(&output.project);
|
||||
state.reconcile_claude_run_dirs(&output.project).await;
|
||||
Ok(ProjectDto::from(output))
|
||||
}
|
||||
|
||||
async fn invoke_load_layout(args: &Value, state: &BackendCore) -> Result<Value, ErrorDto> {
|
||||
@ -8359,6 +8406,15 @@ mod tests {
|
||||
assert!(validate_web_root(create_web_root(), "--web-root").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_root_candidates_include_appimage_resource_dir() {
|
||||
let appdir = PathBuf::from("/tmp/.mount_IdeA_test");
|
||||
let candidates = web_root_candidates_from_appdir(&appdir);
|
||||
|
||||
assert!(candidates.contains(&appdir.join("usr").join("lib").join("IdeA").join("web")));
|
||||
assert!(candidates.contains(&appdir.join("web")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_allowed_origin_is_refused() {
|
||||
let state = state();
|
||||
@ -8472,6 +8528,49 @@ mod tests {
|
||||
assert!(body["root"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorized_open_project_arms_orchestrator_watch() {
|
||||
let config = test_config();
|
||||
let core = Arc::new(BackendCore::build(config.app_data_dir.clone()));
|
||||
let project_id = create_project_on_core(&core, "MCP Armed Open").await;
|
||||
let state = Arc::new(ServerState::with_core(config, core));
|
||||
state.set_pairing_code_for_test("PAIR1234".to_owned());
|
||||
let cookie = pair_and_cookie(Arc::clone(&state)).await;
|
||||
|
||||
assert!(
|
||||
state.app.open_project_ids().is_empty(),
|
||||
"precondition: the project must not already be armed before HTTP open_project"
|
||||
);
|
||||
|
||||
let response = request(
|
||||
Arc::clone(&state),
|
||||
Method::POST,
|
||||
"/api/invoke",
|
||||
json!({ "command": "open_project", "args": { "projectId": project_id } }),
|
||||
&[("cookie", &cookie)],
|
||||
)
|
||||
.await;
|
||||
let (status, body, _) = response_json(response).await;
|
||||
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["id"], project_id);
|
||||
let expected = parse_project_id(&project_id)
|
||||
.expect("test project id parses")
|
||||
.as_uuid()
|
||||
.simple()
|
||||
.to_string();
|
||||
let opened = state
|
||||
.app
|
||||
.open_project_ids()
|
||||
.into_iter()
|
||||
.map(|id| id.as_uuid().simple().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
opened.iter().any(|id| id == &expected),
|
||||
"expected open_project HTTP flow to arm the orchestrator watch"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authorized_layout_invokes_match_tauri_contracts() {
|
||||
let state = state();
|
||||
|
||||
Reference in New Issue
Block a user