1use crate::core::consolidation;
2use crate::core::providers::config::GitLabConfig;
3use crate::core::providers::provider_trait::ProviderParams;
4use crate::core::providers::registry::global_registry;
5use crate::core::providers::{gitlab, ProviderResult};
6use crate::server::tool_trait::ToolContext;
7
8pub fn handle(args: &serde_json::Map<String, serde_json::Value>, ctx: &ToolContext) -> String {
9 let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("");
10
11 match action {
12 "discover" => handle_discover(),
14
15 "query" => handle_registry_query(args, ctx),
17
18 "mcp_resources" => handle_mcp_resources(args),
20
21 "gitlab_issues" => handle_gitlab_issues(args),
23 "gitlab_issue" => handle_gitlab_issue(args),
24 "gitlab_mrs" => handle_gitlab_mrs(args),
25 "gitlab_pipelines" => handle_gitlab_pipelines(args),
26
27 _ => {
28 let available =
29 "discover, query, mcp_resources, gitlab_issues, gitlab_issue, gitlab_mrs, gitlab_pipelines";
30 format!("Unknown action: {action}. Available: {available}")
31 }
32 }
33}
34
35fn handle_discover() -> String {
40 crate::core::providers::init::init_builtin_providers();
41 let infos = global_registry().discover();
42 if infos.is_empty() {
43 return "No providers registered. Set GITHUB_TOKEN or GITLAB_TOKEN.".to_string();
44 }
45
46 let mut out = format!("Registered providers ({}):\n", infos.len());
47 for info in &infos {
48 let status = if info.available {
49 "ready"
50 } else {
51 "unavailable"
52 };
53 out.push_str(&format!(
54 " {} ({}) [{}] actions: {}\n",
55 info.id,
56 info.display_name,
57 status,
58 info.actions.join(", "),
59 ));
60 }
61 out
62}
63
64fn handle_mcp_resources(args: &serde_json::Map<String, serde_json::Value>) -> String {
69 crate::core::providers::init::init_builtin_providers();
70
71 let Some(provider_id) = args.get("provider").and_then(|v| v.as_str()) else {
72 let registry = global_registry();
73 let mcp_providers: Vec<_> = registry
74 .discover()
75 .into_iter()
76 .filter(|p| p.id.starts_with("mcp:"))
77 .collect();
78
79 if mcp_providers.is_empty() {
80 return "No MCP bridges configured. Add [providers.mcp_bridges] to config.toml."
81 .to_string();
82 }
83
84 let mut out = format!("Available MCP bridges ({}):\n", mcp_providers.len());
85 for p in &mcp_providers {
86 let status = if p.available { "ready" } else { "unavailable" };
87 out.push_str(&format!(" {} ({}) [{}]\n", p.id, p.display_name, status));
88 }
89 out.push_str("\nUse provider=\"mcp:<name>\" to list resources from a specific bridge.");
90 return out;
91 };
92
93 let provider_id = if provider_id.starts_with("mcp:") {
94 provider_id.to_string()
95 } else {
96 format!("mcp:{provider_id}")
97 };
98
99 let params = ProviderParams {
100 limit: args
101 .get("limit")
102 .and_then(serde_json::Value::as_u64)
103 .map(|n| n as usize),
104 ..Default::default()
105 };
106
107 match global_registry().execute(&provider_id, "resources", ¶ms) {
108 Ok(result) => format_result(&result),
109 Err(e) => format!("Error: {e}"),
110 }
111}
112
113fn handle_registry_query(
118 args: &serde_json::Map<String, serde_json::Value>,
119 ctx: &ToolContext,
120) -> String {
121 crate::core::providers::init::init_with_project_root(Some(std::path::Path::new(
122 &ctx.project_root,
123 )));
124
125 let Some(provider_id) = args.get("provider").and_then(|v| v.as_str()) else {
126 return "Error: 'provider' is required for action=query".to_string();
127 };
128 let Some(resource) = args.get("resource").and_then(|v| v.as_str()) else {
129 return "Error: 'resource' is required for action=query".to_string();
130 };
131
132 let params = ProviderParams {
133 project: args
134 .get("project")
135 .and_then(|v| v.as_str())
136 .map(String::from),
137 state: args.get("state").and_then(|v| v.as_str()).map(String::from),
138 limit: args
139 .get("limit")
140 .and_then(serde_json::Value::as_u64)
141 .map(|n| n as usize),
142 query: args.get("query").and_then(|v| v.as_str()).map(String::from),
143 id: args.get("id").and_then(|v| v.as_str()).map(String::from),
144 };
145
146 let mode = args
147 .get("mode")
148 .and_then(|v| v.as_str())
149 .unwrap_or("compact");
150
151 match mode {
152 "chunks" => handle_registry_chunks(provider_id, resource, ¶ms, ctx),
153 _ => handle_registry_compact(provider_id, resource, ¶ms, ctx),
154 }
155}
156
157fn handle_registry_compact(
158 provider_id: &str,
159 resource: &str,
160 params: &ProviderParams,
161 ctx: &ToolContext,
162) -> String {
163 match global_registry().execute_as_chunks(provider_id, resource, params) {
164 Ok(chunks) => {
165 consolidate_to_session(&chunks, ctx);
166 let result = global_registry().execute(provider_id, resource, params);
167 match result {
168 Ok(r) => format_result(&r),
169 Err(_) => format_chunks_compact(&chunks, provider_id, resource),
170 }
171 }
172 Err(e) => format!("Error: {e}"),
173 }
174}
175
176fn handle_registry_chunks(
177 provider_id: &str,
178 resource: &str,
179 params: &ProviderParams,
180 ctx: &ToolContext,
181) -> String {
182 match global_registry().execute_as_chunks(provider_id, resource, params) {
183 Ok(chunks) => {
184 consolidate_to_session(&chunks, ctx);
185 let mut out = format!(
186 "{} content chunks from {provider_id}/{resource}:\n",
187 chunks.len()
188 );
189 for c in &chunks {
190 let refs = if c.references.is_empty() {
191 String::new()
192 } else {
193 format!(" refs:[{}]", c.references.join(","))
194 };
195 out.push_str(&format!(
196 " {} {:?} ({}tok){}\n",
197 c.file_path, c.kind, c.token_count, refs
198 ));
199 }
200 out
201 }
202 Err(e) => format!("Error: {e}"),
203 }
204}
205
206fn consolidate_to_session(chunks: &[crate::core::content_chunk::ContentChunk], ctx: &ToolContext) {
216 if chunks.is_empty() {
217 return;
218 }
219
220 let artifacts = consolidation::consolidate(chunks);
221 if artifacts.is_empty() {
222 return;
223 }
224
225 if let Some(cache_lock) = ctx.cache.as_ref() {
227 if let Ok(mut cache) = cache_lock.try_write() {
228 for entry in &artifacts.cache_entries {
229 cache.store(&entry.uri, &entry.content);
230 }
231 }
232 }
233
234 let external_count = artifacts
235 .bm25_chunks
236 .iter()
237 .filter(|c| c.is_external())
238 .count();
239 let edge_count = artifacts.edges.len();
240 let fact_count = artifacts.facts.len();
241 let cache_count = artifacts.cache_entries.len();
242
243 tracing::debug!(
244 "[ctx_provider] consolidated {} chunks → {} edges, {} facts, {} cached",
245 external_count,
246 edge_count,
247 fact_count,
248 cache_count,
249 );
250
251 let cfg = crate::core::config::Config::load();
253 if !cfg.providers.auto_index {
254 return;
255 }
256
257 let project_root = ctx.project_root.clone();
258 std::thread::spawn(move || {
259 apply_artifacts_to_stores(&artifacts, &project_root);
260 });
261}
262
263pub fn apply_artifacts_to_stores(
266 artifacts: &consolidation::ConsolidationArtifacts,
267 project_root: &str,
268) {
269 let root_path = std::path::Path::new(project_root);
270
271 if !artifacts.bm25_chunks.is_empty() {
273 let mut index = crate::core::bm25_index::BM25Index::load_or_build(root_path);
274 let ingested = index.ingest_content_chunks(artifacts.bm25_chunks.clone());
275 if ingested > 0 {
276 if let Err(e) = index.save(root_path) {
277 tracing::warn!("[ctx_provider] BM25 save failed: {e}");
278 } else {
279 tracing::info!("[ctx_provider] indexed {ingested} provider chunks into BM25");
280 }
281 }
282 }
283
284 if !artifacts.edges.is_empty() {
286 let mut graph = crate::core::graph_index::load_or_build(project_root);
287 let added =
288 crate::core::cross_source_edges::merge_edges(&mut graph.edges, artifacts.edges.clone());
289 if added > 0 {
290 if let Err(e) = graph.save() {
291 tracing::warn!("[ctx_provider] graph save failed: {e}");
292 } else {
293 tracing::info!("[ctx_provider] added {added} cross-source edges to graph");
294 }
295 }
296 }
297
298 if !artifacts.facts.is_empty() {
300 let policy = crate::core::memory_policy::MemoryPolicy::default();
301 let mut knowledge = crate::core::knowledge::ProjectKnowledge::load(project_root)
302 .unwrap_or_else(|| crate::core::knowledge::ProjectKnowledge::new(project_root));
303
304 let session_id = format!("provider-ingest-{}", chrono::Utc::now().timestamp());
305 for fact in &artifacts.facts {
306 knowledge.remember(
307 &fact.category,
308 &fact.key,
309 &fact.value,
310 &session_id,
311 fact.confidence,
312 &policy,
313 );
314 }
315
316 if let Err(e) = knowledge.save() {
317 tracing::warn!("[ctx_provider] knowledge save failed: {e}");
318 } else {
319 tracing::info!(
320 "[ctx_provider] remembered {} facts from provider data",
321 artifacts.facts.len()
322 );
323 }
324 }
325}
326
327fn format_chunks_compact(
328 chunks: &[crate::core::content_chunk::ContentChunk],
329 provider_id: &str,
330 resource: &str,
331) -> String {
332 let mut out = format!("{} results from {provider_id}/{resource}:\n", chunks.len());
333 for c in chunks {
334 out.push_str(&format!(
335 " #{} {}\n",
336 c.file_path.rsplit('/').next().unwrap_or("?"),
337 c.symbol_name
338 ));
339 }
340 out
341}
342
343fn handle_gitlab_issues(args: &serde_json::Map<String, serde_json::Value>) -> String {
348 let config = match GitLabConfig::from_env() {
349 Ok(c) => c,
350 Err(e) => return format!("Error: {e}"),
351 };
352 let state = args.get("state").and_then(|v| v.as_str());
353 let labels = args.get("labels").and_then(|v| v.as_str());
354 let limit = args
355 .get("limit")
356 .and_then(serde_json::Value::as_u64)
357 .map(|n| n as usize);
358
359 match gitlab::list_issues(&config, state, labels, limit) {
360 Ok(result) => format_result(&result),
361 Err(e) => format!("Error: {e}"),
362 }
363}
364
365fn handle_gitlab_issue(args: &serde_json::Map<String, serde_json::Value>) -> String {
366 let config = match GitLabConfig::from_env() {
367 Ok(c) => c,
368 Err(e) => return format!("Error: {e}"),
369 };
370 let iid = args
371 .get("iid")
372 .and_then(serde_json::Value::as_u64)
373 .unwrap_or(0);
374 if iid == 0 {
375 return "Error: iid is required for gitlab_issue".to_string();
376 }
377
378 match gitlab::show_issue(&config, iid) {
379 Ok(result) => format_result(&result),
380 Err(e) => format!("Error: {e}"),
381 }
382}
383
384fn handle_gitlab_mrs(args: &serde_json::Map<String, serde_json::Value>) -> String {
385 let config = match GitLabConfig::from_env() {
386 Ok(c) => c,
387 Err(e) => return format!("Error: {e}"),
388 };
389 let state = args.get("state").and_then(|v| v.as_str());
390 let limit = args
391 .get("limit")
392 .and_then(serde_json::Value::as_u64)
393 .map(|n| n as usize);
394
395 match gitlab::list_mrs(&config, state, limit) {
396 Ok(result) => format_result(&result),
397 Err(e) => format!("Error: {e}"),
398 }
399}
400
401fn handle_gitlab_pipelines(args: &serde_json::Map<String, serde_json::Value>) -> String {
402 let config = match GitLabConfig::from_env() {
403 Ok(c) => c,
404 Err(e) => return format!("Error: {e}"),
405 };
406 let status = args.get("status").and_then(|v| v.as_str());
407 let limit = args
408 .get("limit")
409 .and_then(serde_json::Value::as_u64)
410 .map(|n| n as usize);
411
412 match gitlab::list_pipelines(&config, status, limit) {
413 Ok(result) => format_result(&result),
414 Err(e) => format!("Error: {e}"),
415 }
416}
417
418fn format_result(result: &ProviderResult) -> String {
419 crate::core::redaction::redact_text_if_enabled(&result.format_compact())
420}