fix claude-code proxy

This commit is contained in:
2025-06-24 22:55:16 +09:00
parent 26b1b2cf87
commit 7791399314
5 changed files with 199 additions and 4 deletions

View File

@ -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"

View File

@ -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
View 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"]
}
}
}
}))
}

View File

@ -1,5 +1,6 @@
pub mod server;
pub mod tools;
pub mod types;
pub mod claude_proxy;
pub use server::McpServer;

View File

@ -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())
}