codelens-mcp 1.9.10

Pure Rust MCP server for code intelligence — 101 tools (+6 semantic), 25 languages, tree-sitter-first, 50-87% fewer tokens
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#![recursion_limit = "256"]

mod analysis_queue;
mod artifact_store;
mod authority;
mod build_info;
mod client_profile;
mod dispatch;
mod error;
mod job_store;
mod mutation_audit;
mod mutation_gate;
mod preflight_store;
mod prompts;
mod protocol;
mod recent_buffer;
mod resource_analysis;
mod resource_catalog;
mod resource_context;
mod resource_profiles;
mod resources;
mod runtime_types;
mod server;
mod session_context;
mod session_metrics_payload;
mod state;
mod telemetry;
mod test_helpers;
mod tool_defs;
mod tool_runtime;
mod tools;

pub(crate) use state::AppState;

use anyhow::{Context, Result};
use codelens_engine::ProjectRoot;
use server::oneshot::run_oneshot;
use server::transport_stdio::run_stdio;
use state::RuntimeDaemonMode;
use std::path::PathBuf;
use std::sync::Arc;
use tool_defs::{
    ToolPreset, ToolProfile, ToolSurface, default_budget_for_preset, default_budget_for_profile,
};

#[derive(Clone, Debug, PartialEq, Eq)]
enum StartupProjectSource {
    Cli(String),
    ClaudeEnv(String),
    McpEnv(String),
    Cwd(PathBuf),
}

impl StartupProjectSource {
    fn is_explicit(&self) -> bool {
        !matches!(self, Self::Cwd(_))
    }

    fn label(&self) -> &'static str {
        match self {
            Self::Cli(_) => "CLI path",
            Self::ClaudeEnv(_) => "CLAUDE_PROJECT_DIR",
            Self::McpEnv(_) => "MCP_PROJECT_DIR",
            Self::Cwd(_) => "current working directory",
        }
    }
}

fn flag_takes_value(flag: &str) -> bool {
    matches!(
        flag,
        "--preset" | "--profile" | "--daemon-mode" | "--cmd" | "--args" | "--transport" | "--port"
    )
}

pub(crate) fn parse_cli_project_arg(args: &[String]) -> Option<String> {
    let mut skip_next = false;
    let mut iter = args.iter().skip(1);
    while let Some(arg) = iter.next() {
        let value = arg.as_str();
        if skip_next {
            skip_next = false;
            continue;
        }
        if value == "--" {
            return iter.next().map(|entry| entry.to_string());
        }
        if let Some((flag, _)) = value.split_once('=')
            && flag_takes_value(flag)
        {
            continue;
        }
        if flag_takes_value(value) {
            skip_next = true;
            continue;
        }
        if value.starts_with('-') {
            continue;
        }
        return Some(value.to_string());
    }
    None
}

fn select_startup_project_source(
    args: &[String],
    claude_project_dir: Option<String>,
    mcp_project_dir: Option<String>,
    cwd: PathBuf,
) -> StartupProjectSource {
    if let Some(path) = parse_cli_project_arg(args) {
        StartupProjectSource::Cli(path)
    } else if let Some(path) = claude_project_dir {
        StartupProjectSource::ClaudeEnv(path)
    } else if let Some(path) = mcp_project_dir {
        StartupProjectSource::McpEnv(path)
    } else {
        StartupProjectSource::Cwd(cwd)
    }
}

fn resolve_startup_project(source: &StartupProjectSource) -> Result<ProjectRoot> {
    match source {
        StartupProjectSource::Cli(path)
        | StartupProjectSource::ClaudeEnv(path)
        | StartupProjectSource::McpEnv(path) => ProjectRoot::new(path).with_context(|| {
            format!(
                "failed to resolve explicit project root from {}",
                source.label()
            )
        }),
        StartupProjectSource::Cwd(path) => ProjectRoot::new(path)
            .with_context(|| format!("failed to resolve project root from {}", path.display())),
    }
}

fn cli_option_value(args: &[String], flag: &str) -> Option<String> {
    let mut iter = args.iter().skip(1);
    while let Some(arg) = iter.next() {
        if arg == "--" {
            break;
        }
        if let Some(value) = arg.strip_prefix(&format!("{flag}=")) {
            return Some(value.to_owned());
        }
        if arg == flag {
            return iter.next().cloned();
        }
    }
    None
}

/// Phase 4c (§observability): emit a single-line startup marker at
/// `warn` level so append-only log files (e.g. launchd's
/// `~/.codex/codelens-http.log`) have an explicit session boundary
/// between historical noise and the current run. Includes every
/// identity field a debugger might want: `pid`, `transport`, `port`,
/// `project_root`, `project_source` (CLI path / env var / cwd),
/// `surface`, `token_budget`, `daemon_mode`, and the build-time
/// identity fields introduced in Phase 4b (`git_sha`, `build_time`,
/// `git_dirty`) plus the wall-clock `daemon_started_at`.
///
/// `warn!` level is intentional: the default `CODELENS_LOG` filter
/// is `warn`, so session-start markers are visible without users
/// having to opt into `info` logging.
#[cfg_attr(not(feature = "http"), allow(dead_code))]
fn format_http_startup_banner(
    project_root: &std::path::Path,
    project_source: &StartupProjectSource,
    surface_label: &str,
    token_budget: usize,
    daemon_mode: RuntimeDaemonMode,
    port: u16,
    daemon_started_at: &str,
) -> String {
    let escaped_project_root = project_root.display().to_string().replace('"', "\\\"");
    format!(
        "CODELENS_SESSION_START pid={} transport=http port={} project_root=\"{}\" project_source=\"{}\" surface={} token_budget={} daemon_mode={} git_sha={} build_time={} daemon_started_at={} git_dirty={}",
        std::process::id(),
        port,
        escaped_project_root,
        project_source.label(),
        surface_label,
        token_budget,
        daemon_mode.as_str(),
        crate::build_info::BUILD_GIT_SHA,
        crate::build_info::BUILD_TIME,
        daemon_started_at,
        crate::build_info::build_git_dirty()
    )
}

fn main() -> Result<()> {
    // Initialize tracing subscriber — output to stderr to avoid interfering with
    // stdio JSON-RPC transport on stdout. Controlled via CODELENS_LOG env var.
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_env("CODELENS_LOG")
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
        )
        .with_writer(std::io::stderr)
        .with_target(false)
        .init();

    let args: Vec<String> = std::env::args().collect();
    let preset = args
        .iter()
        .position(|a| a == "--preset")
        .and_then(|i| args.get(i + 1))
        .map(|s| ToolPreset::from_str(s))
        .or_else(|| {
            std::env::var("CODELENS_PRESET")
                .ok()
                .map(|s| ToolPreset::from_str(&s))
        })
        .unwrap_or_else(|| state::ClientProfile::detect(None).default_preset());
    let profile = cli_option_value(&args, "--profile")
        .as_deref()
        .and_then(ToolProfile::from_str)
        .or_else(|| {
            std::env::var("CODELENS_PROFILE")
                .ok()
                .and_then(|s| ToolProfile::from_str(&s))
        });
    let daemon_mode = cli_option_value(&args, "--daemon-mode")
        .as_deref()
        .map(RuntimeDaemonMode::from_str)
        .or_else(|| {
            std::env::var("CODELENS_DAEMON_MODE")
                .ok()
                .map(|s| RuntimeDaemonMode::from_str(&s))
        })
        .unwrap_or(RuntimeDaemonMode::Standard);

    // Project root resolution priority:
    // 1. Explicit path argument (if not ".")
    // 2. CLAUDE_PROJECT_DIR environment variable (set by Claude Code)
    // 3. MCP_PROJECT_DIR environment variable (generic)
    // 4. Current working directory with .git/.cargo marker detection
    let project_from_claude = std::env::var("CLAUDE_PROJECT_DIR").ok();
    let project_from_mcp = std::env::var("MCP_PROJECT_DIR").ok();
    let cwd = std::env::current_dir()?;
    let project_source = select_startup_project_source(
        &args,
        project_from_claude.clone(),
        project_from_mcp.clone(),
        cwd,
    );

    // One-shot CLI mode: --cmd <tool_name> [--args '<json>']
    let cmd_tool = cli_option_value(&args, "--cmd");

    let cmd_args = cli_option_value(&args, "--args");

    let transport = cli_option_value(&args, "--transport").unwrap_or_else(|| "stdio".to_owned());

    #[cfg(feature = "http")]
    let port: u16 = cli_option_value(&args, "--port")
        .and_then(|s| s.parse().ok())
        .unwrap_or(7837);

    let project = resolve_startup_project(&project_source)?;
    if !project_source.is_explicit() && project.as_path() == std::path::Path::new("/") {
        anyhow::bail!(
            "Refusing to start CodeLens on `/` without an explicit project root. Pass a path or set MCP_PROJECT_DIR/CLAUDE_PROJECT_DIR."
        );
    }

    // v1.5 Phase 2j MCP follow-up: auto-detect the dominant language so
    // `CODELENS_EMBED_HINT_AUTO=1` alone (without an explicit
    // `CODELENS_EMBED_HINT_AUTO_LANG`) becomes the v1.6.0 default flip
    // candidate. Applies to both one-shot CLI (`--cmd`) and stdio MCP.
    // `activate_project` calls the same helper for MCP-driven switches.
    crate::tools::session::auto_set_embed_hint_lang(project.as_path());

    let app_state = AppState::new(project, preset);
    app_state.configure_transport_mode(&transport);
    app_state.configure_daemon_mode(daemon_mode);
    if let Some(profile) = profile {
        app_state.set_surface(ToolSurface::Profile(profile));
        app_state.set_token_budget(default_budget_for_profile(profile));
    } else {
        app_state.set_surface(ToolSurface::Preset(preset));
        app_state.set_token_budget(default_budget_for_preset(preset));
    }

    #[cfg(feature = "http")]
    if transport == "http" {
        let startup_banner = format_http_startup_banner(
            app_state.project().as_path(),
            &project_source,
            app_state.surface().as_label(),
            app_state.token_budget(),
            app_state.daemon_mode(),
            port,
            app_state.daemon_started_at(),
        );
        // Intentionally `warn!`: the default CODELENS_LOG filter is `warn`,
        // so a session-start marker must be visible without requiring users
        // to opt into `info` logging. This gives appended daemon logs an
        // explicit boundary between historical noise and the current run.
        tracing::warn!("{startup_banner}");
    }

    // One-shot mode: run a single tool and exit
    if let Some(tool_name) = cmd_tool {
        let state = Arc::new(app_state);
        return run_oneshot(&state, &tool_name, cmd_args.as_deref());
    }

    match transport.as_str() {
        #[cfg(feature = "http")]
        "http" => {
            let state = Arc::new(app_state.with_session_store());
            server::transport_http::run_http(state, port)
        }
        #[cfg(not(feature = "http"))]
        "http" => {
            anyhow::bail!(
                "HTTP transport requires the `http` feature. Rebuild with: cargo build --features http"
            );
        }
        _ => run_stdio(Arc::new(app_state)),
    }
}

#[cfg(test)]
mod startup_tests {
    use super::{StartupProjectSource, parse_cli_project_arg, resolve_startup_project};

    fn temp_dir(name: &str) -> std::path::PathBuf {
        let dir = std::env::temp_dir().join(format!(
            "codelens-startup-{name}-{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn cli_project_arg_skips_flag_values() {
        let args = vec![
            "codelens-mcp".to_owned(),
            "--transport".to_owned(),
            "http".to_owned(),
            "--profile".to_owned(),
            "reviewer-graph".to_owned(),
            "/tmp/repo".to_owned(),
        ];
        assert_eq!(parse_cli_project_arg(&args).as_deref(), Some("/tmp/repo"));
    }

    #[test]
    fn cli_project_arg_honors_double_dash_separator() {
        let args = vec![
            "codelens-mcp".to_owned(),
            "--transport".to_owned(),
            "http".to_owned(),
            "--".to_owned(),
            ".".to_owned(),
        ];
        assert_eq!(parse_cli_project_arg(&args).as_deref(), Some("."));
    }

    #[test]
    fn cli_project_arg_skips_equals_syntax_flags() {
        let args = vec![
            "codelens-mcp".to_owned(),
            "--transport=http".to_owned(),
            "--port=7842".to_owned(),
            "/tmp/repo".to_owned(),
        ];
        assert_eq!(parse_cli_project_arg(&args).as_deref(), Some("/tmp/repo"));
    }

    #[test]
    fn explicit_project_resolution_fails_closed() {
        let missing = temp_dir("missing-parent").join("does-not-exist");
        let source = StartupProjectSource::Cli(missing.to_string_lossy().to_string());
        let error = resolve_startup_project(&source).expect_err("missing explicit path must fail");
        assert!(
            error
                .to_string()
                .contains("failed to resolve explicit project root")
        );
    }

    /// Phase 4c (§observability): the startup banner must carry
    /// every identity field a debugger might want in a single line,
    /// so append-only log tails can pinpoint "which build, which
    /// process, which project" without cross-referencing other
    /// state. Guards the format string against accidental field
    /// removal.
    #[test]
    fn http_startup_banner_includes_runtime_identity_fields() {
        let banner = super::format_http_startup_banner(
            std::path::Path::new("/tmp/repo"),
            &StartupProjectSource::McpEnv("/tmp/repo".to_owned()),
            "builder-minimal",
            2400,
            crate::state::RuntimeDaemonMode::Standard,
            7837,
            "2026-04-11T19:49:55Z",
        );
        assert!(banner.starts_with("CODELENS_SESSION_START pid="));
        assert!(banner.contains("transport=http"));
        assert!(banner.contains("port=7837"));
        assert!(banner.contains("project_root=\"/tmp/repo\""));
        assert!(banner.contains("project_source=\"MCP_PROJECT_DIR\""));
        assert!(banner.contains("surface=builder-minimal"));
        assert!(banner.contains("token_budget=2400"));
        assert!(banner.contains("daemon_mode=standard"));
        assert!(banner.contains("daemon_started_at=2026-04-11T19:49:55Z"));
        assert!(banner.contains("git_sha="));
        assert!(banner.contains("build_time="));
        assert!(banner.contains("git_dirty="));
    }
}

#[path = "integration_tests/mod.rs"]
#[cfg(test)]
mod tests;