1#[cfg(feature = "https-server")]
7pub async fn serve_https(config: crate::Settings, watch: bool, bind: String) -> anyhow::Result<()> {
8 use crate::IndexPersistence;
9 use crate::indexing::facade::IndexFacade;
10 use crate::mcp::{CodeIntelligenceServer, notifications::NotificationBroadcaster};
11 use crate::watcher::HotReloadWatcher;
12 use anyhow::Context;
13 use axum::Router;
14 use axum_server::tls_rustls::RustlsConfig;
15 use rmcp::transport::streamable_http_server::{
16 StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
17 };
18 use std::net::SocketAddr;
19 use std::path::PathBuf;
20 use std::sync::Arc;
21 use std::time::Duration;
22 use tokio::sync::RwLock;
23 use tokio_util::sync::CancellationToken;
24
25 crate::logging::init_with_config(&config.logging);
27
28 crate::log_event!("https", "starting", "MCP server on {bind}");
29
30 let broadcaster = Arc::new(NotificationBroadcaster::new(100));
32
33 let settings = Arc::new(config.clone());
35 let persistence = IndexPersistence::new(config.index_path.clone());
36
37 let facade = if persistence.exists() {
38 match persistence.load_facade(settings.clone()) {
39 Ok(loaded) => {
40 let symbol_count = loaded.symbol_count();
41 crate::log_event!("https", "loaded", "{symbol_count} symbols");
42 loaded
43 }
44 Err(e) => {
45 tracing::warn!("[https] failed to load index: {e}");
46 crate::log_event!("https", "starting", "empty index");
47 IndexFacade::new(settings.clone()).expect("Failed to create IndexFacade")
48 }
49 }
50 } else {
51 crate::log_event!("https", "starting", "no existing index");
52 IndexFacade::new(settings.clone()).expect("Failed to create IndexFacade")
53 };
54 let indexer = Arc::new(RwLock::new(facade));
55
56 let ct = CancellationToken::new();
58
59 let document_store_arc = crate::documents::load_from_settings(&config);
61 if document_store_arc.is_some() {
62 tracing::debug!(target: "mcp", "document store loaded for MCP server");
63 }
64
65 if watch || config.file_watch.enabled {
67 use crate::watcher::UnifiedWatcher;
68 use crate::watcher::handlers::{CodeFileHandler, ConfigFileHandler, DocumentFileHandler};
69
70 let workspace_root = config
71 .workspace_root
72 .clone()
73 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
74
75 let settings_path = workspace_root.join(".codanna/settings.toml");
76 let debounce_ms = config.file_watch.debounce_ms;
77
78 let mut builder = UnifiedWatcher::builder()
80 .broadcaster(broadcaster.clone())
81 .indexer(indexer.clone())
82 .index_path(config.index_path.clone())
83 .workspace_root(workspace_root.clone())
84 .debounce_ms(debounce_ms);
85
86 builder = builder.handler(CodeFileHandler::new(
88 indexer.clone(),
89 workspace_root.clone(),
90 ));
91
92 match ConfigFileHandler::new(settings_path.clone()) {
94 Ok(config_handler) => {
95 builder = builder.handler(config_handler);
96 }
97 Err(e) => {
98 tracing::warn!("[config] failed to create handler: {e}");
99 }
100 }
101
102 if let Some(ref store_arc) = document_store_arc {
104 tracing::debug!(target: "mcp", "adding document handler to watcher");
105 builder = builder
106 .document_store(store_arc.clone())
107 .chunking_config(config.documents.defaults.clone())
108 .handler(DocumentFileHandler::new(
109 store_arc.clone(),
110 workspace_root.clone(),
111 ));
112 }
113
114 match builder.build() {
116 Ok(unified_watcher) => {
117 let watcher_ct = ct.clone();
118 tokio::spawn(async move {
119 tokio::select! {
120 result = unified_watcher.watch() => {
121 if let Err(e) = result {
122 tracing::error!("[watcher] error: {e}");
123 }
124 }
125 _ = watcher_ct.cancelled() => {
126 crate::log_event!("watcher", "stopped");
127 }
128 }
129 });
130 crate::log_event!(
131 "watcher",
132 "started",
133 "debounce: {debounce_ms}ms, config: {}",
134 settings_path.display()
135 );
136 }
137 Err(e) => {
138 tracing::warn!("[watcher] failed to start: {e}");
139 tracing::warn!("[watcher] continuing without file watching");
140 }
141 }
142 }
143
144 if watch {
146 let hot_reload_indexer = indexer.clone();
147 let hot_reload_settings = Arc::new(config.clone());
148 let hot_reload_broadcaster = broadcaster.clone();
149 let hot_reload_ct = ct.clone();
150
151 let watch_interval = 5u64;
153
154 let hot_reload_watcher = HotReloadWatcher::new(
155 hot_reload_indexer,
156 hot_reload_settings,
157 Duration::from_secs(watch_interval),
158 )
159 .with_broadcaster(hot_reload_broadcaster);
160
161 tokio::spawn(async move {
162 tokio::select! {
163 _ = hot_reload_watcher.watch() => {
164 crate::log_event!("hot-reload", "ended");
165 }
166 _ = hot_reload_ct.cancelled() => {
167 crate::log_event!("hot-reload", "stopped");
168 }
169 }
170 });
171
172 crate::log_event!("hot-reload", "started", "polling every {watch_interval}s");
173 }
174
175 let indexer_for_service = indexer.clone();
180 let config_for_service = Arc::new(config.clone());
181
182 let shared_service =
184 CodeIntelligenceServer::new_with_facade(indexer_for_service, config_for_service);
185
186 let shared_service = if let Some(store_arc) = document_store_arc {
188 tracing::debug!(target: "mcp", "attaching document store to MCP server");
189 shared_service.with_document_store_arc(store_arc)
190 } else {
191 shared_service
192 };
193
194 let notification_receiver = broadcaster.subscribe();
196 let notification_server = shared_service.clone();
197 tokio::spawn(async move {
198 notification_server
199 .start_notification_listener(notification_receiver)
200 .await;
201 });
202
203 let mcp_service = StreamableHttpService::new(
204 move || {
205 Ok(shared_service.clone())
209 },
210 LocalSessionManager::default().into(),
211 StreamableHttpServerConfig {
212 cancellation_token: ct.child_token(),
213 sse_keep_alive: Some(Duration::from_secs(15)),
214 sse_retry: None,
215 stateful_mode: true,
216 json_response: false,
217 },
218 );
219
220 let bind_for_metadata = bind.clone();
222 let oauth_metadata = move || async move {
223 eprintln!("OAuth metadata endpoint called");
224 axum::Json(serde_json::json!({
225 "issuer": format!("https://{}", bind_for_metadata.clone()),
226 "authorization_endpoint": format!("https://{}/oauth/authorize", bind_for_metadata.clone()),
227 "token_endpoint": format!("https://{}/oauth/token", bind_for_metadata.clone()),
228 "registration_endpoint": format!("https://{}/oauth/register", bind_for_metadata),
229 "scopes_supported": ["mcp"],
230 "response_types_supported": ["code"],
231 "grant_types_supported": ["authorization_code", "refresh_token"],
232 "code_challenge_methods_supported": ["S256", "plain"],
233 "token_endpoint_auth_methods_supported": ["none"]
234 }))
235 };
236
237 async fn log_requests(
239 req: axum::extract::Request,
240 next: axum::middleware::Next,
241 ) -> Result<axum::response::Response, axum::http::StatusCode> {
242 let path = req.uri().path();
243 eprintln!("Request to: {path}");
244
245 eprintln!("Headers received:");
247 for (name, value) in req.headers() {
248 if let Ok(v) = value.to_str() {
249 eprintln!(" {name}: {v}");
250 }
251 }
252
253 Ok(next.run(req).await)
255 }
256
257 let mcp_router_with_logging = Router::new()
259 .nest_service("/mcp", mcp_service)
260 .layer(axum::middleware::from_fn(log_requests));
261
262 let router = Router::new()
264 .route(
266 "/.well-known/oauth-authorization-server",
267 axum::routing::get(oauth_metadata),
268 )
269 .route("/oauth/register", axum::routing::post(oauth_register))
270 .route("/oauth/token", axum::routing::post(oauth_token))
271 .route("/oauth/authorize", axum::routing::get(oauth_authorize))
272 .route("/health", axum::routing::get(health_check))
274 .merge(mcp_router_with_logging);
276
277 let (cert_pem, key_pem) = get_or_create_certificate(&bind)
279 .await
280 .context("Failed to get or create TLS certificate")?;
281
282 let tls_config = RustlsConfig::from_pem(cert_pem, key_pem)
284 .await
285 .context("Failed to configure TLS")?;
286
287 let addr: SocketAddr = bind.parse().context("Failed to parse bind address")?;
289
290 eprintln!("HTTPS MCP server listening on https://{bind}");
291 eprintln!("MCP endpoint: https://{bind}/mcp");
292 eprintln!("Health check: https://{bind}/health");
293 eprintln!();
294 eprintln!("Using self-signed certificate. Clients will show security warnings.");
295 eprintln!("To trust the certificate, visit https://{bind} in your browser first");
296 eprintln!();
297 eprintln!("Press Ctrl+C to stop the server");
298
299 let server = axum_server::bind_rustls(addr, tls_config).serve(router.into_make_service());
301
302 tokio::select! {
304 result = server => {
305 result?;
306 }
307 _ = shutdown_signal() => {
308 eprintln!("Shutting down HTTPS server...");
309 ct.cancel();
310 }
311 }
312
313 eprintln!("HTTPS server shut down gracefully");
314 Ok(())
315}
316
317#[cfg(feature = "https-server")]
319async fn health_check() -> &'static str {
320 eprintln!("Health check endpoint called");
321 "OK"
322}
323
324#[cfg(feature = "https-server")]
326async fn oauth_register(
327 axum::Json(payload): axum::Json<serde_json::Value>,
328) -> axum::Json<serde_json::Value> {
329 eprintln!("OAuth register endpoint called with: {payload:?}");
330 axum::Json(serde_json::json!({
333 "client_id": "dummy-client-id",
334 "client_secret": "", "client_id_issued_at": 1234567890,
336 "grant_types": ["authorization_code", "refresh_token"],
337 "response_types": ["code"],
338 "redirect_uris": payload.get("redirect_uris").unwrap_or(&serde_json::json!([])).clone(),
339 "client_name": payload.get("client_name").unwrap_or(&serde_json::json!("MCP Client")).clone(),
340 "token_endpoint_auth_method": "none"
341 }))
342}
343
344#[cfg(feature = "https-server")]
346async fn oauth_token(body: String) -> axum::Json<serde_json::Value> {
347 eprintln!("OAuth token endpoint called with body: {body}");
348
349 let params: std::collections::HashMap<String, String> =
351 serde_urlencoded::from_str(&body).unwrap_or_default();
352
353 eprintln!("Token request params: {params:?}");
354
355 let grant_type = params.get("grant_type").cloned().unwrap_or_default();
357 let code = params.get("code").cloned().unwrap_or_default();
358
359 if grant_type == "refresh_token" {
361 eprintln!("Rejecting refresh_token grant type");
362 return axum::Json(serde_json::json!({
363 "error": "unsupported_grant_type",
364 "error_description": "only authorization_code is supported"
365 }));
366 }
367
368 if grant_type == "authorization_code" && code == "dummy-auth-code" {
370 axum::Json(serde_json::json!({
372 "access_token": "mcp-access-token-dummy",
373 "token_type": "Bearer",
374 "expires_in": 3600,
375 "scope": "mcp"
376 }))
377 } else {
378 eprintln!("Invalid token request: grant_type={grant_type}, code={code}");
380 axum::Json(serde_json::json!({
381 "error": "invalid_grant",
382 "error_description": "Invalid authorization code or grant type"
383 }))
384 }
385}
386
387#[cfg(feature = "https-server")]
389async fn oauth_authorize(
390 axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
391) -> impl axum::response::IntoResponse {
392 eprintln!("OAuth authorize endpoint called with params: {params:?}");
393
394 let redirect_uri = params
396 .get("redirect_uri")
397 .cloned()
398 .unwrap_or_else(|| "http://localhost:3118/callback".to_string());
399 let state = params.get("state").cloned().unwrap_or_default();
400
401 let callback_url = format!("{redirect_uri}?code=dummy-auth-code&state={state}");
403
404 let html = format!(
406 r#"
407<!DOCTYPE html>
408<html>
409<head>
410 <title>Authorize Codanna</title>
411 <meta charset="utf-8">
412 <style>
413 body {{
414 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
415 display: flex;
416 justify-content: center;
417 align-items: center;
418 height: 100vh;
419 margin: 0;
420 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
421 }}
422 .container {{
423 background: white;
424 padding: 2rem;
425 border-radius: 10px;
426 box-shadow: 0 10px 40px rgba(0,0,0,0.2);
427 text-align: center;
428 max-width: 400px;
429 }}
430 h1 {{
431 color: #333;
432 margin-bottom: 1rem;
433 }}
434 p {{
435 color: #666;
436 margin-bottom: 2rem;
437 }}
438 button {{
439 background: #667eea;
440 color: white;
441 border: none;
442 padding: 12px 30px;
443 border-radius: 5px;
444 font-size: 16px;
445 cursor: pointer;
446 transition: background 0.3s;
447 }}
448 button:hover {{
449 background: #764ba2;
450 }}
451 .spinner {{
452 margin: 20px auto;
453 width: 50px;
454 height: 50px;
455 border: 3px solid #f3f3f3;
456 border-top: 3px solid #667eea;
457 border-radius: 50%;
458 animation: spin 1s linear infinite;
459 }}
460 @keyframes spin {{
461 0% {{ transform: rotate(0deg); }}
462 100% {{ transform: rotate(360deg); }}
463 }}
464 </style>
465 <script>
466 // Auto-redirect after a short delay
467 setTimeout(function() {{
468 window.location.href = "{callback_url}";
469 }}, 1500);
470 </script>
471</head>
472<body>
473 <div class="container">
474 <h1>🔐 Authorize Codanna</h1>
475 <div class="spinner"></div>
476 <p>Authorizing access to Codanna MCP Server...</p>
477 <p>You will be redirected automatically.</p>
478 <button onclick="window.location.href='{callback_url}'">
479 Continue Manually
480 </button>
481 </div>
482</body>
483</html>
484"#
485 );
486
487 axum::response::Html(html)
488}
489
490#[cfg(feature = "https-server")]
492async fn shutdown_signal() {
493 tokio::signal::ctrl_c()
494 .await
495 .expect("failed to listen for ctrl+c");
496 eprintln!("Received shutdown signal");
497}
498
499#[cfg(feature = "https-server")]
501async fn get_or_create_certificate(bind: &str) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
502 use anyhow::Context;
503 use rcgen::generate_simple_self_signed;
504
505 let cert_dir = dirs::config_dir()
507 .context("Failed to get config directory")?
508 .join("codanna")
509 .join("certs");
510
511 let cert_path = cert_dir.join("server.pem");
512 let key_path = cert_dir.join("server.key");
513
514 tokio::fs::create_dir_all(&cert_dir)
516 .await
517 .context("Failed to create certificate directory")?;
518
519 if cert_path.exists() && key_path.exists() {
521 eprintln!("Loading existing certificates from {cert_dir:?}");
522 let cert = tokio::fs::read(&cert_path)
523 .await
524 .context("Failed to read certificate file")?;
525 let key = tokio::fs::read(&key_path)
526 .await
527 .context("Failed to read key file")?;
528 return Ok((cert, key));
529 }
530
531 eprintln!("Generating new enhanced self-signed certificate...");
532
533 let mut subject_alt_names = vec![
535 "localhost".to_string(),
536 "127.0.0.1".to_string(),
537 "::1".to_string(),
538 ];
539
540 if bind.starts_with("0.0.0.0") {
542 if let Ok(local_ip) = local_ip_address::local_ip() {
543 eprintln!("Including local network IP in certificate: {local_ip}");
544 subject_alt_names.push(local_ip.to_string());
545 }
546 }
547
548 let cert = generate_simple_self_signed(subject_alt_names.clone())
550 .context("Failed to generate self-signed certificate")?;
551
552 let cert_pem = cert.cert.pem().into_bytes();
553 let key_pem = cert.signing_key.serialize_pem().into_bytes();
554
555 tokio::fs::write(&cert_path, &cert_pem)
557 .await
558 .context("Failed to write server certificate")?;
559 tokio::fs::write(&key_path, &key_pem)
560 .await
561 .context("Failed to write server key")?;
562
563 use std::collections::hash_map::DefaultHasher;
565 use std::hash::{Hash, Hasher};
566
567 let mut hasher = DefaultHasher::new();
568 cert.cert.der().hash(&mut hasher);
569 let fingerprint = hasher.finish();
570 let fingerprint_hex = format!("{fingerprint:016X}");
571
572 eprintln!();
573 eprintln!("🔐 Certificate Details:");
574 eprintln!(" - Type: Self-Signed TLS Certificate");
575 eprintln!(" - Location: {}", cert_path.display());
576 eprintln!(" - Fingerprint: {fingerprint_hex}");
577 eprintln!(" - Valid for: {}", subject_alt_names.join(", "));
578 eprintln!();
579 eprintln!("🔧 To trust this certificate on macOS:");
580 eprintln!();
581 eprintln!(" Option 1: Command line (requires sudo):");
582 eprintln!(
583 " sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain {}",
584 cert_path.display()
585 );
586 eprintln!();
587 eprintln!(" Option 2: GUI (recommended):");
588 eprintln!(" 1. Open Finder and navigate to: {}", cert_dir.display());
589 eprintln!(" 2. Double-click 'server.pem'");
590 eprintln!(" 3. Add to 'System' keychain");
591 eprintln!(" 4. Set to 'Always Trust' for SSL");
592 eprintln!();
593 eprintln!(" Option 3: Open in browser first:");
594 eprintln!(" 1. Visit https://127.0.0.1:8443/health in Safari/Chrome");
595 eprintln!(" 2. Click 'Advanced' and proceed anyway");
596 eprintln!(" 3. This may help some clients accept the certificate");
597 eprintln!();
598 eprintln!("⚠️ After trusting the certificate, restart Claude Code to reconnect");
599 eprintln!();
600
601 Ok((cert_pem, key_pem))
602}
603
604#[cfg(feature = "https-server")]
606mod local_ip_address {
607 use std::net::{IpAddr, UdpSocket};
608
609 pub fn local_ip() -> Result<IpAddr, Box<dyn std::error::Error>> {
610 let socket = UdpSocket::bind("0.0.0.0:0")?;
614 socket.connect("8.8.8.8:80")?;
615 let addr = socket.local_addr()?;
616 Ok(addr.ip())
617 }
618}
619
620#[cfg(not(feature = "https-server"))]
621pub async fn serve_https(
622 _config: crate::Settings,
623 _watch: bool,
624 _bind: String,
625) -> anyhow::Result<()> {
626 eprintln!("HTTPS server support is not compiled in.");
627 eprintln!("Please rebuild with: cargo build --features https-server");
628 std::process::exit(1);
629}