fix claude-code proxy
This commit is contained in:
		| @@ -39,6 +39,8 @@ urlencoding = "2.1" | ||||
| axum = "0.7" | ||||
| tower = "0.5" | ||||
| tower-http = { version = "0.5", features = ["cors", "fs"] } | ||||
| axum-extra = { version = "0.9", features = ["typed-header"] } | ||||
| tracing = "0.1" | ||||
| hyper = { version = "1.0", features = ["full"] } | ||||
| tower-sessions = "0.12" | ||||
| jsonwebtoken = "9.2" | ||||
|   | ||||
							
								
								
									
										24
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -79,6 +79,15 @@ enum Commands { | ||||
|         /// Path to the blog directory | ||||
|         #[arg(default_value = ".")] | ||||
|         path: PathBuf, | ||||
|         /// Enable Claude proxy mode | ||||
|         #[arg(long)] | ||||
|         claude_proxy: bool, | ||||
|         /// API token for Claude proxy authentication | ||||
|         #[arg(long)] | ||||
|         api_token: Option<String>, | ||||
|         /// Claude Code executable path | ||||
|         #[arg(long, default_value = "claude")] | ||||
|         claude_code_path: String, | ||||
|     }, | ||||
|     /// Generate documentation from code | ||||
|     Doc(commands::doc::DocCommand), | ||||
| @@ -203,9 +212,20 @@ async fn main() -> Result<()> { | ||||
|             std::env::set_current_dir(path)?; | ||||
|             commands::clean::execute().await?; | ||||
|         } | ||||
|         Commands::Mcp { port, path } => { | ||||
|         Commands::Mcp { port, path, claude_proxy, api_token, claude_code_path } => { | ||||
|             use crate::mcp::McpServer; | ||||
|             let server = McpServer::new(path); | ||||
|             let mut server = McpServer::new(path); | ||||
|              | ||||
|             if claude_proxy { | ||||
|                 let token = api_token | ||||
|                     .or_else(|| std::env::var("CLAUDE_PROXY_API_TOKEN").ok()) | ||||
|                     .ok_or_else(|| { | ||||
|                         anyhow::anyhow!("API token is required when --claude-proxy is enabled. Set CLAUDE_PROXY_API_TOKEN environment variable or use --api-token") | ||||
|                     })?; | ||||
|                 server = server.with_claude_proxy(token, Some(claude_code_path.clone())); | ||||
|                 println!("Claude proxy mode enabled - using Claude Code executable: {}", claude_code_path); | ||||
|             } | ||||
|              | ||||
|             server.serve(port).await?; | ||||
|         } | ||||
|         Commands::Doc(doc_cmd) => { | ||||
|   | ||||
							
								
								
									
										156
									
								
								src/mcp/claude_proxy.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/mcp/claude_proxy.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| use anyhow::Result; | ||||
| use axum::{ | ||||
|     extract::State, | ||||
|     http::StatusCode, | ||||
|     response::Json, | ||||
| }; | ||||
| use axum_extra::{ | ||||
|     headers::{authorization::Bearer, Authorization}, | ||||
|     TypedHeader, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json::{json, Value}; | ||||
| // Removed unused import | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct ChatRequest { | ||||
|     pub question: String, | ||||
|     #[serde(rename = "systemPrompt")] | ||||
|     pub system_prompt: String, | ||||
|     #[serde(default)] | ||||
|     pub context: Value, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct ChatResponse { | ||||
|     pub answer: String, | ||||
| } | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct ClaudeProxyState { | ||||
|     pub api_token: String, | ||||
|     pub claude_code_path: String, | ||||
| } | ||||
|  | ||||
| pub async fn claude_chat_handler( | ||||
|     State(state): State<crate::mcp::server::AppState>, | ||||
|     auth: Option<TypedHeader<Authorization<Bearer>>>, | ||||
|     Json(request): Json<ChatRequest>, | ||||
| ) -> Result<Json<ChatResponse>, StatusCode> { | ||||
|     // Claude proxyが有効かチェック | ||||
|     let claude_proxy = state.claude_proxy.as_ref().ok_or(StatusCode::NOT_FOUND)?; | ||||
|      | ||||
|     // 認証チェック | ||||
|     let auth = auth.ok_or(StatusCode::UNAUTHORIZED)?; | ||||
|     if auth.token() != claude_proxy.api_token { | ||||
|         return Err(StatusCode::UNAUTHORIZED); | ||||
|     } | ||||
|  | ||||
|     // Claude CodeのMCP通信実装 | ||||
|     let response = communicate_with_claude_mcp( | ||||
|         &request.question, | ||||
|         &request.system_prompt, | ||||
|         &request.context, | ||||
|         &claude_proxy.claude_code_path, | ||||
|     ).await?; | ||||
|  | ||||
|     Ok(Json(ChatResponse { answer: response })) | ||||
| } | ||||
|  | ||||
| async fn communicate_with_claude_mcp( | ||||
|     message: &str, | ||||
|     system: &str, | ||||
|     _context: &Value, | ||||
|     claude_code_path: &str, | ||||
| ) -> Result<String, StatusCode> { | ||||
|     tracing::info!("Communicating with Claude Code via stdio"); | ||||
|     tracing::info!("Message: {}", message); | ||||
|     tracing::info!("System prompt: {}", system); | ||||
|      | ||||
|     // Claude Code MCPプロセスを起動 | ||||
|     // Use the full path to avoid shell function and don't use --continue | ||||
|     let claude_executable = if claude_code_path == "claude" { | ||||
|         "/Users/syui/.claude/local/claude" | ||||
|     } else { | ||||
|         claude_code_path | ||||
|     }; | ||||
|      | ||||
|     let mut child = tokio::process::Command::new(claude_executable) | ||||
|         .args(&["--print", "--output-format", "text"]) | ||||
|         .stdin(std::process::Stdio::piped()) | ||||
|         .stdout(std::process::Stdio::piped()) | ||||
|         .stderr(std::process::Stdio::piped()) | ||||
|         .spawn() | ||||
|         .map_err(|e| { | ||||
|             tracing::error!("Failed to start Claude Code process: {}", e); | ||||
|             StatusCode::INTERNAL_SERVER_ERROR | ||||
|         })?; | ||||
|      | ||||
|     // プロンプトを構築 | ||||
|     let full_prompt = if !system.is_empty() { | ||||
|         format!("{}\n\nUser: {}", system, message) | ||||
|     } else { | ||||
|         message.to_string() | ||||
|     }; | ||||
|      | ||||
|     // 標準入力にプロンプトを送信 | ||||
|     if let Some(stdin) = child.stdin.take() { | ||||
|         use tokio::io::AsyncWriteExt; | ||||
|         let mut stdin = stdin; | ||||
|         stdin.write_all(full_prompt.as_bytes()).await.map_err(|e| { | ||||
|             tracing::error!("Failed to write to Claude Code stdin: {}", e); | ||||
|             StatusCode::INTERNAL_SERVER_ERROR | ||||
|         })?; | ||||
|         stdin.shutdown().await.map_err(|e| { | ||||
|             tracing::error!("Failed to close Claude Code stdin: {}", e); | ||||
|             StatusCode::INTERNAL_SERVER_ERROR | ||||
|         })?; | ||||
|     } | ||||
|      | ||||
|     // プロセス完了を待機(タイムアウト付き) | ||||
|     let output = tokio::time::timeout( | ||||
|         tokio::time::Duration::from_secs(30), | ||||
|         child.wait_with_output() | ||||
|     ) | ||||
|     .await | ||||
|     .map_err(|_| { | ||||
|         tracing::error!("Claude Code process timed out"); | ||||
|         StatusCode::REQUEST_TIMEOUT | ||||
|     })? | ||||
|     .map_err(|e| { | ||||
|         tracing::error!("Claude Code process failed: {}", e); | ||||
|         StatusCode::INTERNAL_SERVER_ERROR | ||||
|     })?; | ||||
|      | ||||
|     // プロセス終了ステータスをチェック | ||||
|     if !output.status.success() { | ||||
|         let stderr = String::from_utf8_lossy(&output.stderr); | ||||
|         tracing::error!("Claude Code process failed with stderr: {}", stderr); | ||||
|         return Ok("Claude Codeプロセスでエラーが発生しました".to_string()); | ||||
|     } | ||||
|      | ||||
|     // 標準出力を解析 | ||||
|     let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|     tracing::debug!("Claude Code stdout: {}", stdout); | ||||
|      | ||||
|     // Claude Codeは通常プレーンテキストを返すので、そのまま返す | ||||
|     Ok(stdout.trim().to_string()) | ||||
| } | ||||
|  | ||||
| pub async fn claude_tools_handler() -> Json<Value> { | ||||
|     Json(json!({ | ||||
|         "tools": { | ||||
|             "chat": { | ||||
|                 "description": "Chat with Claude", | ||||
|                 "inputSchema": { | ||||
|                     "type": "object", | ||||
|                     "properties": { | ||||
|                         "message": {"type": "string"}, | ||||
|                         "system": {"type": "string"} | ||||
|                     }, | ||||
|                     "required": ["message"] | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     })) | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| pub mod server; | ||||
| pub mod tools; | ||||
| pub mod types; | ||||
| pub mod claude_proxy; | ||||
|  | ||||
| pub use server::McpServer; | ||||
| @@ -12,10 +12,12 @@ use std::sync::Arc; | ||||
| use tower_http::cors::CorsLayer; | ||||
| use crate::mcp::tools::BlogTools; | ||||
| use crate::mcp::types::{McpRequest, McpResponse, McpError, CreatePostRequest, ListPostsRequest, BuildRequest}; | ||||
| use crate::mcp::claude_proxy::{claude_chat_handler, claude_tools_handler, ClaudeProxyState}; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| pub struct AppState { | ||||
|     blog_tools: Arc<BlogTools>, | ||||
|     pub blog_tools: Arc<BlogTools>, | ||||
|     pub claude_proxy: Option<Arc<ClaudeProxyState>>, | ||||
| } | ||||
|  | ||||
| pub struct McpServer { | ||||
| @@ -25,17 +27,31 @@ pub struct McpServer { | ||||
| impl McpServer { | ||||
|     pub fn new(base_path: PathBuf) -> Self { | ||||
|         let blog_tools = Arc::new(BlogTools::new(base_path)); | ||||
|         let app_state = AppState { blog_tools }; | ||||
|         let app_state = AppState {  | ||||
|             blog_tools, | ||||
|             claude_proxy: None, | ||||
|         }; | ||||
|          | ||||
|         Self { app_state } | ||||
|     } | ||||
|  | ||||
|     pub fn with_claude_proxy(mut self, api_token: String, claude_code_path: Option<String>) -> Self { | ||||
|         let claude_code_path = claude_code_path.unwrap_or_else(|| "claude".to_string()); | ||||
|         self.app_state.claude_proxy = Some(Arc::new(ClaudeProxyState {  | ||||
|             api_token, | ||||
|             claude_code_path, | ||||
|         })); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn create_router(&self) -> Router { | ||||
|         Router::new() | ||||
|             .route("/", get(root_handler)) | ||||
|             .route("/mcp/tools/list", get(list_tools)) | ||||
|             .route("/mcp/tools/call", post(call_tool)) | ||||
|             .route("/health", get(health_check)) | ||||
|             .route("/api/claude-mcp", post(claude_chat_handler)) | ||||
|             .route("/claude/tools", get(claude_tools_handler)) | ||||
|             .layer(CorsLayer::permissive()) | ||||
|             .with_state(self.app_state.clone()) | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user