use crate::AppState;
use crate::client_profile::ClientProfile;
use crate::dispatch::dispatch_tool;
use crate::prompts::{get_prompt, prompts};
use crate::protocol::{JsonRpcRequest, JsonRpcResponse};
use crate::resources::{read_resource, resources};
use crate::tool_defs::{
is_deferred_control_tool, preferred_bootstrap_tools, preferred_namespaces,
preferred_tier_labels, tool_namespace, tool_tier_label, visible_tools,
};
use serde_json::{Map, Value, json};
use std::collections::BTreeSet;
fn visible_axes_from_tools(
tools: &[&'static crate::protocol::Tool],
) -> (Vec<&'static str>, Vec<&'static str>) {
let mut namespaces = BTreeSet::new();
let mut tiers = BTreeSet::new();
for tool in tools {
namespaces.insert(tool_namespace(tool.name));
tiers.insert(tool_tier_label(tool.name));
}
(
namespaces.into_iter().collect(),
tiers.into_iter().collect(),
)
}
fn merged_string_set<'a>(base: impl IntoIterator<Item = &'a str>, extra: &[&str]) -> Vec<String> {
let mut merged = base
.into_iter()
.map(ToOwned::to_owned)
.collect::<BTreeSet<_>>();
for value in extra {
merged.insert((*value).to_owned());
}
merged.into_iter().collect()
}
fn list_param_bool(request: &JsonRpcRequest, camel: &str, snake: &str) -> Option<bool> {
request
.params
.as_ref()
.and_then(|params| params.get(camel).or_else(|| params.get(snake)))
.and_then(|value| value.as_bool())
}
fn list_param_str<'a>(request: &'a JsonRpcRequest, key: &str) -> Option<&'a str> {
request
.params
.as_ref()
.and_then(|params| params.get(key))
.and_then(|value| value.as_str())
}
pub(crate) fn handle_request(state: &AppState, request: JsonRpcRequest) -> Option<JsonRpcResponse> {
if request.jsonrpc != "2.0" {
return Some(JsonRpcResponse::error(
request.id,
-32600,
"Unsupported jsonrpc version",
));
}
// JSON-RPC 2.0: notifications (no id) MUST NOT receive a response
let is_notification = request.id.is_none();
match request.method.as_str() {
// Notifications — silently accept, never respond
"notifications/initialized"
| "notifications/cancelled"
| "notifications/progress"
| "notifications/roots/list_changed" => None,
"initialize" => Some(JsonRpcResponse::result(
request.id,
json!({
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"resources": { "listChanged": false },
"prompts": { "listChanged": false }
},
"serverInfo": {
"name": "codelens-mcp",
"version": env!("CARGO_PKG_VERSION")
},
"instructions": "CodeLens is a compressed context provider for agent harnesses. Prefer problem-first workflow entrypoints such as explore_codebase, review_architecture, analyze_change_impact, plan_safe_refactor, audit_security_context, trace_request_path, and cleanup_duplicate_logic before expanding raw symbols or graph data. Legacy report tools remain available, but the workflow-first surface is the default path. Keep the visible context bounded, and use get_analysis_section or analysis resources only when you need one section in more detail. For longer reports, start_analysis_job and poll with get_analysis_job."
}),
)),
"resources/list" => Some(JsonRpcResponse::result(
request.id,
json!({ "resources": resources(state) }),
)),
"resources/read" => {
let uri = request
.params
.as_ref()
.and_then(|p| p.get("uri"))
.and_then(|v| v.as_str())
.unwrap_or("");
Some(JsonRpcResponse::result(
request.id,
read_resource(state, uri, request.params.as_ref()),
))
}
"prompts/list" => Some(JsonRpcResponse::result(
request.id,
json!({ "prompts": prompts() }),
)),
"prompts/get" => {
let name = request
.params
.as_ref()
.and_then(|p| p.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("");
let args = request
.params
.as_ref()
.and_then(|p| p.get("arguments"))
.cloned()
.unwrap_or(json!({}));
Some(JsonRpcResponse::result(
request.id,
get_prompt(state, name, &args),
))
}
"tools/list" => {
let session = request
.params
.as_ref()
.map(crate::session_context::SessionRequestContext::from_json)
.unwrap_or_default();
let surface = state.execution_surface(&session);
let all_tools = visible_tools(surface);
let (all_namespaces, all_tiers) = visible_axes_from_tools(&all_tools);
let requested_namespace = request
.params
.as_ref()
.and_then(|params| params.get("namespace"))
.and_then(|value| value.as_str());
let requested_tier = request
.params
.as_ref()
.and_then(|params| params.get("tier"))
.and_then(|value| value.as_str());
let full_listing = request
.params
.as_ref()
.and_then(|params| params.get("full"))
.and_then(|value| value.as_bool())
.unwrap_or(false);
let deferred_loading_requested = request
.params
.as_ref()
.and_then(|params| params.get("_session_deferred_tool_loading"))
.and_then(|value| value.as_bool());
let client_profile = list_param_str(&request, "_session_client_name")
.map(|name| ClientProfile::detect(Some(name)))
.unwrap_or(ClientProfile::Generic);
let deferred_loading_requested = deferred_loading_requested
.or_else(|| client_profile.default_deferred_tool_loading())
.unwrap_or(false);
let loaded_namespaces = request
.params
.as_ref()
.and_then(|params| params.get("_session_loaded_namespaces"))
.and_then(|value| value.as_array())
.map(|values| {
values
.iter()
.filter_map(|value| value.as_str())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let loaded_tiers = request
.params
.as_ref()
.and_then(|params| params.get("_session_loaded_tiers"))
.and_then(|value| value.as_array())
.map(|values| {
values
.iter()
.filter_map(|value| value.as_str())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let full_tool_exposure = request
.params
.as_ref()
.and_then(|params| params.get("_session_full_tool_exposure"))
.and_then(|value| value.as_bool())
.unwrap_or(false);
let preferred_namespaces = preferred_namespaces(surface);
let preferred_bootstrap = preferred_bootstrap_tools(surface);
let preferred_tiers = preferred_tier_labels(surface);
let deferred_loading_active = deferred_loading_requested
&& requested_namespace.is_none()
&& requested_tier.is_none()
&& !full_listing
&& !full_tool_exposure;
let lean_contract = client_profile.default_tool_contract_mode() == "lean"
&& !full_listing
&& !full_tool_exposure;
let include_output_schema =
list_param_bool(&request, "includeOutputSchema", "include_output_schema")
.unwrap_or(!(deferred_loading_active || lean_contract));
let include_annotations =
list_param_bool(&request, "includeAnnotations", "include_annotations")
.unwrap_or(!lean_contract);
let filtered = all_tools
.iter()
.copied()
.filter(|tool| match requested_namespace {
_ if deferred_loading_active && is_deferred_control_tool(tool.name) => true,
Some(namespace) => tool_namespace(tool.name) == namespace,
None if deferred_loading_active => {
let namespace = tool_namespace(tool.name);
preferred_namespaces.contains(&namespace)
|| loaded_namespaces.contains(&namespace)
}
None => true,
})
.filter(|tool| match requested_tier {
_ if deferred_loading_active && is_deferred_control_tool(tool.name) => true,
Some(tier) => tool_tier_label(tool.name) == tier,
None if deferred_loading_active => {
let tier = tool_tier_label(tool.name);
preferred_tiers.contains(&tier) || loaded_tiers.contains(&tier)
}
None => true,
})
.filter(|tool| match preferred_bootstrap {
_ if deferred_loading_active && is_deferred_control_tool(tool.name) => true,
Some(tool_names) if deferred_loading_active => tool_names.contains(&tool.name),
_ => true,
})
.collect::<Vec<_>>();
let response_tools = filtered
.iter()
.map(|tool| {
let mut tool = (*tool).clone();
if !include_output_schema {
tool.output_schema = None;
}
if !include_annotations {
tool.annotations = None;
}
tool
})
.collect::<Vec<_>>();
let effective_namespaces =
merged_string_set(preferred_namespaces.iter().copied(), &loaded_namespaces);
let effective_tiers = merged_string_set(preferred_tiers.iter().copied(), &loaded_tiers);
let token_estimate = if include_output_schema {
filtered.iter().map(|tool| tool.estimated_tokens).sum()
} else {
serde_json::to_string(&response_tools)
.map(|body| body.len() / 4)
.unwrap_or(0)
};
if deferred_loading_requested
&& (requested_namespace.is_some() || requested_tier.is_some())
{
state.metrics().record_deferred_namespace_expansion();
}
state.metrics().record_call_with_tokens(
"tools/list",
0,
true,
token_estimate,
surface.as_label(),
false,
None,
);
let mut payload = Map::new();
payload.insert(
"client_profile".to_owned(),
Value::String(client_profile.as_str().to_owned()),
);
payload.insert(
"active_surface".to_owned(),
Value::String(surface.as_label().to_owned()),
);
payload.insert(
"preferred_namespaces".to_owned(),
json!(preferred_namespaces),
);
payload.insert("preferred_tiers".to_owned(), json!(preferred_tiers));
payload.insert("loaded_namespaces".to_owned(), json!(loaded_namespaces));
payload.insert("loaded_tiers".to_owned(), json!(loaded_tiers));
payload.insert(
"effective_namespaces".to_owned(),
json!(effective_namespaces),
);
payload.insert("effective_tiers".to_owned(), json!(effective_tiers));
payload.insert(
"deferred_loading_active".to_owned(),
Value::Bool(deferred_loading_active),
);
payload.insert(
"include_output_schema".to_owned(),
Value::Bool(include_output_schema),
);
payload.insert(
"include_annotations".to_owned(),
Value::Bool(include_annotations),
);
payload.insert(
"default_contract_mode".to_owned(),
Value::String(client_profile.default_tool_contract_mode().to_owned()),
);
payload.insert("tool_count".to_owned(), json!(response_tools.len()));
payload.insert("tool_count_total".to_owned(), json!(all_tools.len()));
payload.insert("tools".to_owned(), json!(response_tools));
if !lean_contract {
payload.insert("visible_namespaces".to_owned(), json!(all_namespaces));
payload.insert("visible_tiers".to_owned(), json!(all_tiers));
payload.insert("full_listing".to_owned(), Value::Bool(full_listing));
payload.insert(
"full_tool_exposure".to_owned(),
Value::Bool(full_tool_exposure),
);
}
if let Some(namespace) = requested_namespace {
payload.insert(
"selected_namespace".to_owned(),
Value::String(namespace.to_owned()),
);
}
if let Some(tier) = requested_tier {
payload.insert("selected_tier".to_owned(), Value::String(tier.to_owned()));
}
Some(JsonRpcResponse::result(request.id, Value::Object(payload)))
}
"tools/call" => match request.params {
Some(params) => Some(dispatch_tool(state, request.id, params)),
None => Some(JsonRpcResponse::error(request.id, -32602, "Missing params")),
},
// Unknown notification — silently ignore per JSON-RPC 2.0
_ if is_notification => None,
method => Some(JsonRpcResponse::error(
request.id,
-32601,
format!("Method not found: {method}"),
)),
}
}