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())?
48 }
49 }
50 } else {
51 crate::log_event!("https", "starting", "no existing index");
52 IndexFacade::new(settings.clone())?
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 {
212 let cfg = StreamableHttpServerConfig::default()
213 .with_cancellation_token(ct.child_token())
214 .with_sse_keep_alive(Some(Duration::from_secs(15)))
215 .with_sse_retry(None)
216 .with_stateful_mode(true)
217 .with_json_response(false);
218 let cfg = match config.mcp.allowed_hosts.clone() {
219 Some(hosts) => cfg.with_allowed_hosts(hosts),
220 None => cfg,
221 };
222 match config.mcp.allowed_origins.clone() {
223 Some(origins) => cfg.with_allowed_origins(origins),
224 None => cfg,
225 }
226 },
227 );
228
229 let bind_for_metadata = bind.clone();
231 let oauth_metadata = move || async move {
232 eprintln!("OAuth metadata endpoint called");
233 axum::Json(serde_json::json!({
234 "issuer": format!("https://{}", bind_for_metadata.clone()),
235 "authorization_endpoint": format!("https://{}/oauth/authorize", bind_for_metadata.clone()),
236 "token_endpoint": format!("https://{}/oauth/token", bind_for_metadata.clone()),
237 "registration_endpoint": format!("https://{}/oauth/register", bind_for_metadata),
238 "scopes_supported": ["mcp"],
239 "response_types_supported": ["code"],
240 "grant_types_supported": ["authorization_code", "refresh_token"],
241 "code_challenge_methods_supported": ["S256", "plain"],
242 "token_endpoint_auth_methods_supported": ["none"]
243 }))
244 };
245
246 async fn log_requests(
248 req: axum::extract::Request,
249 next: axum::middleware::Next,
250 ) -> Result<axum::response::Response, axum::http::StatusCode> {
251 let path = req.uri().path();
252 eprintln!("Request to: {path}");
253
254 eprintln!("Headers received:");
256 for (name, value) in req.headers() {
257 if let Ok(v) = value.to_str() {
258 eprintln!(" {name}: {v}");
259 }
260 }
261
262 Ok(next.run(req).await)
264 }
265
266 let mcp_router_with_logging = Router::new()
268 .nest_service("/mcp", mcp_service)
269 .layer(axum::middleware::from_fn(log_requests));
270
271 let router = Router::new()
273 .route(
275 "/.well-known/oauth-authorization-server",
276 axum::routing::get(oauth_metadata),
277 )
278 .route("/oauth/register", axum::routing::post(oauth_register))
279 .route("/oauth/token", axum::routing::post(oauth_token))
280 .route("/oauth/authorize", axum::routing::get(oauth_authorize))
281 .route("/health", axum::routing::get(health_check))
283 .merge(mcp_router_with_logging);
285
286 let (cert_pem, key_pem) = get_or_create_certificate(&bind)
288 .await
289 .context("Failed to get or create TLS certificate")?;
290
291 let tls_config = RustlsConfig::from_pem(cert_pem, key_pem)
293 .await
294 .context("Failed to configure TLS")?;
295
296 let addr: SocketAddr = bind.parse().context("Failed to parse bind address")?;
298
299 eprintln!("HTTPS MCP server listening on https://{bind}");
300 eprintln!("MCP endpoint: https://{bind}/mcp");
301 eprintln!("Health check: https://{bind}/health");
302 eprintln!();
303 eprintln!("Using self-signed certificate. Clients will show security warnings.");
304 eprintln!("To trust the certificate, visit https://{bind} in your browser first");
305 eprintln!();
306 eprintln!("Press Ctrl+C to stop the server");
307
308 let server = axum_server::bind_rustls(addr, tls_config).serve(router.into_make_service());
310
311 tokio::select! {
313 result = server => {
314 result?;
315 }
316 _ = shutdown_signal() => {
317 eprintln!("Shutting down HTTPS server...");
318 ct.cancel();
319 }
320 }
321
322 eprintln!("HTTPS server shut down gracefully");
323 Ok(())
324}
325
326#[cfg(feature = "https-server")]
328async fn health_check() -> &'static str {
329 eprintln!("Health check endpoint called");
330 "OK"
331}
332
333#[cfg(feature = "https-server")]
335async fn oauth_register(
336 axum::Json(payload): axum::Json<serde_json::Value>,
337) -> axum::Json<serde_json::Value> {
338 eprintln!("OAuth register endpoint called with: {payload:?}");
339 axum::Json(serde_json::json!({
342 "client_id": "dummy-client-id",
343 "client_secret": "", "client_id_issued_at": 1234567890,
345 "grant_types": ["authorization_code", "refresh_token"],
346 "response_types": ["code"],
347 "redirect_uris": payload.get("redirect_uris").unwrap_or(&serde_json::json!([])).clone(),
348 "client_name": payload.get("client_name").unwrap_or(&serde_json::json!("MCP Client")).clone(),
349 "token_endpoint_auth_method": "none"
350 }))
351}
352
353#[cfg(feature = "https-server")]
355async fn oauth_token(body: String) -> axum::Json<serde_json::Value> {
356 eprintln!("OAuth token endpoint called with body: {body}");
357
358 let params: std::collections::HashMap<String, String> =
360 serde_urlencoded::from_str(&body).unwrap_or_default();
361
362 eprintln!("Token request params: {params:?}");
363
364 let grant_type = params.get("grant_type").cloned().unwrap_or_default();
366 let code = params.get("code").cloned().unwrap_or_default();
367
368 if grant_type == "refresh_token" {
370 eprintln!("Rejecting refresh_token grant type");
371 return axum::Json(serde_json::json!({
372 "error": "unsupported_grant_type",
373 "error_description": "only authorization_code is supported"
374 }));
375 }
376
377 if grant_type == "authorization_code" && code == "dummy-auth-code" {
379 axum::Json(serde_json::json!({
381 "access_token": "mcp-access-token-dummy",
382 "token_type": "Bearer",
383 "expires_in": 3600,
384 "scope": "mcp"
385 }))
386 } else {
387 eprintln!("Invalid token request: grant_type={grant_type}, code={code}");
389 axum::Json(serde_json::json!({
390 "error": "invalid_grant",
391 "error_description": "Invalid authorization code or grant type"
392 }))
393 }
394}
395
396#[cfg(feature = "https-server")]
398async fn oauth_authorize(
399 axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
400) -> impl axum::response::IntoResponse {
401 eprintln!("OAuth authorize endpoint called with params: {params:?}");
402
403 let redirect_uri = params
405 .get("redirect_uri")
406 .cloned()
407 .unwrap_or_else(|| "http://localhost:3118/callback".to_string());
408 let state = params.get("state").cloned().unwrap_or_default();
409
410 let callback_url = format!("{redirect_uri}?code=dummy-auth-code&state={state}");
412
413 let html = format!(
415 r#"
416<!DOCTYPE html>
417<html>
418<head>
419 <title>Authorize Codanna</title>
420 <meta charset="utf-8">
421 <style>
422 body {{
423 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
424 display: flex;
425 justify-content: center;
426 align-items: center;
427 height: 100vh;
428 margin: 0;
429 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
430 }}
431 .container {{
432 background: white;
433 padding: 2rem;
434 border-radius: 10px;
435 box-shadow: 0 10px 40px rgba(0,0,0,0.2);
436 text-align: center;
437 max-width: 400px;
438 }}
439 h1 {{
440 color: #333;
441 margin-bottom: 1rem;
442 }}
443 p {{
444 color: #666;
445 margin-bottom: 2rem;
446 }}
447 button {{
448 background: #667eea;
449 color: white;
450 border: none;
451 padding: 12px 30px;
452 border-radius: 5px;
453 font-size: 16px;
454 cursor: pointer;
455 transition: background 0.3s;
456 }}
457 button:hover {{
458 background: #764ba2;
459 }}
460 .spinner {{
461 margin: 20px auto;
462 width: 50px;
463 height: 50px;
464 border: 3px solid #f3f3f3;
465 border-top: 3px solid #667eea;
466 border-radius: 50%;
467 animation: spin 1s linear infinite;
468 }}
469 @keyframes spin {{
470 0% {{ transform: rotate(0deg); }}
471 100% {{ transform: rotate(360deg); }}
472 }}
473 </style>
474 <script>
475 // Auto-redirect after a short delay
476 setTimeout(function() {{
477 window.location.href = "{callback_url}";
478 }}, 1500);
479 </script>
480</head>
481<body>
482 <div class="container">
483 <h1>🔐 Authorize Codanna</h1>
484 <div class="spinner"></div>
485 <p>Authorizing access to Codanna MCP Server...</p>
486 <p>You will be redirected automatically.</p>
487 <button onclick="window.location.href='{callback_url}'">
488 Continue Manually
489 </button>
490 </div>
491</body>
492</html>
493"#
494 );
495
496 axum::response::Html(html)
497}
498
499#[cfg(feature = "https-server")]
501async fn shutdown_signal() {
502 tokio::signal::ctrl_c()
503 .await
504 .expect("failed to listen for ctrl+c");
505 eprintln!("Received shutdown signal");
506}
507
508#[cfg(feature = "https-server")]
510async fn get_or_create_certificate(bind: &str) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
511 use anyhow::Context;
512 use rcgen::generate_simple_self_signed;
513
514 let cert_dir = dirs::config_dir()
516 .context("Failed to get config directory")?
517 .join("codanna")
518 .join("certs");
519
520 let cert_path = cert_dir.join("server.pem");
521 let key_path = cert_dir.join("server.key");
522
523 tokio::fs::create_dir_all(&cert_dir)
525 .await
526 .context("Failed to create certificate directory")?;
527
528 if cert_path.exists() && key_path.exists() {
530 eprintln!("Loading existing certificates from {cert_dir:?}");
531 let cert = tokio::fs::read(&cert_path)
532 .await
533 .context("Failed to read certificate file")?;
534 let key = tokio::fs::read(&key_path)
535 .await
536 .context("Failed to read key file")?;
537 return Ok((cert, key));
538 }
539
540 eprintln!("Generating new enhanced self-signed certificate...");
541
542 let mut subject_alt_names = vec![
544 "localhost".to_string(),
545 "127.0.0.1".to_string(),
546 "::1".to_string(),
547 ];
548
549 if bind.starts_with("0.0.0.0") {
551 if let Ok(local_ip) = local_ip_address::local_ip() {
552 eprintln!("Including local network IP in certificate: {local_ip}");
553 subject_alt_names.push(local_ip.to_string());
554 }
555 }
556
557 let cert = generate_simple_self_signed(subject_alt_names.clone())
559 .context("Failed to generate self-signed certificate")?;
560
561 let cert_pem = cert.cert.pem().into_bytes();
562 let key_pem = cert.signing_key.serialize_pem().into_bytes();
563
564 tokio::fs::write(&cert_path, &cert_pem)
566 .await
567 .context("Failed to write server certificate")?;
568 tokio::fs::write(&key_path, &key_pem)
569 .await
570 .context("Failed to write server key")?;
571
572 use std::collections::hash_map::DefaultHasher;
574 use std::hash::{Hash, Hasher};
575
576 let mut hasher = DefaultHasher::new();
577 cert.cert.der().hash(&mut hasher);
578 let fingerprint = hasher.finish();
579 let fingerprint_hex = format!("{fingerprint:016X}");
580
581 eprintln!();
582 eprintln!("🔐 Certificate Details:");
583 eprintln!(" - Type: Self-Signed TLS Certificate");
584 eprintln!(" - Location: {}", cert_path.display());
585 eprintln!(" - Fingerprint: {fingerprint_hex}");
586 eprintln!(" - Valid for: {}", subject_alt_names.join(", "));
587 eprintln!();
588 eprintln!("🔧 To trust this certificate on macOS:");
589 eprintln!();
590 eprintln!(" Option 1: Command line (requires sudo):");
591 eprintln!(
592 " sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain {}",
593 cert_path.display()
594 );
595 eprintln!();
596 eprintln!(" Option 2: GUI (recommended):");
597 eprintln!(" 1. Open Finder and navigate to: {}", cert_dir.display());
598 eprintln!(" 2. Double-click 'server.pem'");
599 eprintln!(" 3. Add to 'System' keychain");
600 eprintln!(" 4. Set to 'Always Trust' for SSL");
601 eprintln!();
602 eprintln!(" Option 3: Open in browser first:");
603 eprintln!(" 1. Visit https://127.0.0.1:8443/health in Safari/Chrome");
604 eprintln!(" 2. Click 'Advanced' and proceed anyway");
605 eprintln!(" 3. This may help some clients accept the certificate");
606 eprintln!();
607 eprintln!("⚠️ After trusting the certificate, restart Claude Code to reconnect");
608 eprintln!();
609
610 Ok((cert_pem, key_pem))
611}
612
613#[cfg(feature = "https-server")]
615mod local_ip_address {
616 use std::net::{IpAddr, UdpSocket};
617
618 pub fn local_ip() -> Result<IpAddr, Box<dyn std::error::Error>> {
619 let socket = UdpSocket::bind("0.0.0.0:0")?;
623 socket.connect("8.8.8.8:80")?;
624 let addr = socket.local_addr()?;
625 Ok(addr.ip())
626 }
627}
628
629#[cfg(not(feature = "https-server"))]
630pub async fn serve_https(
631 _config: crate::Settings,
632 _watch: bool,
633 _bind: String,
634) -> anyhow::Result<()> {
635 eprintln!("HTTPS server support is not compiled in.");
636 eprintln!("Please rebuild with: cargo build --features https-server");
637 std::process::exit(1);
638}