// src/commands/mcp.rs use std::fs; use std::path::{PathBuf}; use std::process::Command as OtherCommand; use serde_json::json; use seahorse::{Command, Context, Flag, FlagType}; use crate::chat::ask_chat; use crate::git::{git_init, git_status}; use crate::config::ConfigPaths; use crate::commands::git_repo::read_all_git_files; use crate::metrics::{load_user_data, save_user_data}; use crate::memory::{log_message}; pub fn mcp_setup() { let config = ConfigPaths::new(); let dest_dir = config.base_dir.join("mcp"); let repo_url = "https://github.com/microsoft/MCP.git"; println!("📁 MCP ディレクトリ: {}", dest_dir.display()); // 1. git clone(もしまだなければ) if !dest_dir.exists() { let status = OtherCommand::new("git") .args(&["clone", repo_url, dest_dir.to_str().unwrap()]) .status() .expect("git clone に失敗しました"); assert!(status.success(), "git clone 実行時にエラーが発生しました"); } let asset_base = PathBuf::from("mcp"); let files_to_copy = vec![ "cli.py", "setup.py", "scripts/ask.py", "scripts/summarize.py", "scripts/context_loader.py", "scripts/prompt_template.py", ]; for rel_path in files_to_copy { let src = asset_base.join(rel_path); let dst = dest_dir.join(rel_path); if let Some(parent) = dst.parent() { let _ = fs::create_dir_all(parent); } if let Err(e) = fs::copy(&src, &dst) { eprintln!("❌ コピー失敗: {} → {}: {}", src.display(), dst.display(), e); } else { println!("✅ コピー: {} → {}", src.display(), dst.display()); } } // venvの作成 let venv_path = dest_dir.join(".venv"); if !venv_path.exists() { println!("🐍 仮想環境を作成しています..."); let output = OtherCommand::new("python3") .args(&["-m", "venv", ".venv"]) .current_dir(&dest_dir) .output() .expect("venvの作成に失敗しました"); if !output.status.success() { eprintln!("❌ venv作成エラー: {}", String::from_utf8_lossy(&output.stderr)); return; } } // `pip install -e .` を仮想環境で実行 let pip_path = if cfg!(target_os = "windows") { dest_dir.join(".venv/Scripts/pip.exe").to_string_lossy().to_string() } else { dest_dir.join(".venv/bin/pip").to_string_lossy().to_string() }; println!("📦 必要なパッケージをインストールしています..."); let output = OtherCommand::new(&pip_path) .arg("install") .arg("openai") .arg("requests") .arg("fastmcp") .arg("uvicorn") .arg("fastapi") .arg("fastapi_mcp") .arg("mcp") .current_dir(&dest_dir) .output() .expect("pip install に失敗しました"); if !output.status.success() { eprintln!( "❌ pip エラー: {}\n{}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout) ); return; } println!("📦 pip install -e . を実行します..."); let output = OtherCommand::new(&pip_path) .arg("install") .arg("-e") .arg(".") .current_dir(&dest_dir) .output() .expect("pip install に失敗しました"); if output.status.success() { println!("🎉 MCP セットアップが完了しました!"); } else { eprintln!( "❌ pip エラー: {}\n{}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout) ); } } fn set_api_key_cmd() -> Command { Command::new("set-api") .description("OpenAI APIキーを設定") .usage("mcp set-api --api <API_KEY>") .flag(Flag::new("api", FlagType::String).description("OpenAI APIキー").alias("a")) .action(|c: &Context| { if let Ok(api_key) = c.string_flag("api") { let config = ConfigPaths::new(); let path = config.base_dir.join("openai.json"); let json_data = json!({ "token": api_key }); if let Err(e) = fs::write(&path, serde_json::to_string_pretty(&json_data).unwrap()) { eprintln!("❌ ファイル書き込み失敗: {}", e); } else { println!("✅ APIキーを保存しました: {}", path.display()); } } else { eprintln!("❗ APIキーを --api で指定してください"); } }) } fn chat_cmd() -> Command { Command::new("chat") .description("チャットで質問を送る") .usage("mcp chat '質問内容' --host <OLLAMA_HOST> --model <MODEL> [--provider <ollama|openai>] [--api-key <KEY>] [--repo <REPO_URL>]") .flag( Flag::new("host", FlagType::String) .description("OLLAMAホストのURL") .alias("H"), ) .flag( Flag::new("model", FlagType::String) .description("モデル名 (OLLAMA_MODEL / OPENAI_MODEL)") .alias("m"), ) .flag( Flag::new("provider", FlagType::String) .description("使用するプロバイダ (ollama / openai)") .alias("p"), ) .flag( Flag::new("api-key", FlagType::String) .description("OpenAI APIキー") .alias("k"), ) .flag( Flag::new("repo", FlagType::String) .description("Gitリポジトリのパスを指定 (すべてのコードを読み込む)") .alias("r"), ) .action(|c: &Context| { let config = ConfigPaths::new(); let user_path = config.data_file("json"); let mut user = load_user_data(&user_path); // repoがある場合は、コードベース読み込みモード if let Ok(repo_url) = c.string_flag("repo") { let repo_base = config.base_dir.join("repos"); let repo_dir = repo_base.join(sanitize_repo_name(&repo_url)); if !repo_dir.exists() { println!("📥 Gitリポジトリをクローン中: {}", repo_url); let status = OtherCommand::new("git") .args(&["clone", &repo_url, repo_dir.to_str().unwrap()]) .status() .expect("❌ Gitのクローンに失敗しました"); assert!(status.success(), "Git clone エラー"); } else { println!("✔ リポジトリはすでに存在します: {}", repo_dir.display()); } let files = read_all_git_files(repo_dir.to_str().unwrap()); let prompt = format!( "以下のコードベースを読み込んで、改善案や次のステップを提案してください:\n{}", files ); if let Some(response) = ask_chat(c, &prompt) { println!("💬 提案:\n{}", response); } else { eprintln!("❗ 提案が取得できませんでした"); } return; } // 通常のチャット処理(repoが指定されていない場合) match c.args.get(0) { Some(question) => { log_message(&config.base_dir, "user", question); let response = ask_chat(c, question); if let Some(ref text) = response { println!("💬 応答:\n{}", text); // 返答内容に基づいて増減(返答の感情解析) if text.contains("thank") || text.contains("great") { user.metrics.trust += 0.05; } else if text.contains("hate") || text.contains("bad") { user.metrics.trust -= 0.05; } log_message(&config.base_dir, "ai", &text); save_user_data(&user_path, &user); } else { eprintln!("❗ 応答が取得できませんでした"); } } None => { eprintln!("❗ 質問が必要です: mcp chat 'こんにちは'"); } } }) } fn init_cmd() -> Command { Command::new("init") .description("Git 初期化") .usage("mcp init") .action(|_| { git_init(); }) } fn status_cmd() -> Command { Command::new("status") .description("Git ステータス表示") .usage("mcp status") .action(|_| { git_status(); }) } fn setup_cmd() -> Command { Command::new("setup") .description("MCP の初期セットアップ") .usage("mcp setup") .action(|_| { mcp_setup(); }) } pub fn mcp_cmd() -> Command { Command::new("mcp") .description("MCP操作コマンド") .usage("mcp <subcommand>") .alias("m") .command(chat_cmd()) .command(init_cmd()) .command(status_cmd()) .command(setup_cmd()) .command(set_api_key_cmd()) } // ファイル名として安全な形に変換 fn sanitize_repo_name(repo_url: &str) -> String { repo_url.replace("://", "_").replace("/", "_").replace("@", "_") }