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