1use crate::core::data_source::DataSource;
5use crate::shell::exp::NewArgs;
6use crate::shell::ingest::{IngestSource, ingest_hook_string};
7use crate::shell::{cli, exp, init, insights, metrics, retro, sync};
8use rmcp::ServerHandler;
9use rmcp::handler::server::wrapper::Parameters;
10use rmcp::model::{CallToolResult, Content, ErrorData};
11use rmcp::schemars;
12use rmcp::tool;
13use rmcp::tool_handler;
14use rmcp::tool_router;
15use serde::Deserialize;
16
17const MCP_CAPABILITIES: &str = r#"Kaizen MCP exposes most `kaizen` CLI workflows as tools. Shell-only today: doctor, guidance, gc, completions, proxy run, telemetry subcommands (init, doctor, pull, print-schema, configure, print-effective-config).
19
20- kaizen_summary — Session counts, USD cost, by-agent/model, top tools. Use for spend and volume. Optional json=true.
21- kaizen_metrics — Code hotspots, slow tools (p95), token-heavy tools, churn. Use for **repository** and tool latency. Optional json.
22- kaizen_sessions_list / kaizen_session_show — Session list and one session metadata. Optional json on list; optional `limit` caps rows (newest first). `kaizen_exp_report` supports `refresh: true` for a full transcript rescan before computing the report (matches CLI `kaizen exp report --refresh`).
23- mcp/search_sessions — BM25 event search over current workspace. Supports since, agent, kind, limit.
24- kaizen_insights — Activity dashboard (7d). kaizen_retro — weekly bets. kaizen_exp_* — experiments.
25- List/summary/insights/metrics/retro are cache-first; set refresh=true to force a full transcript rescan (matches CLI --refresh).
26- sessions_list/summary/insights/metrics also accept all_workspaces=true to aggregate across registered workspace-local DBs.
27- kaizen_ingest_hook — same as `kaizen ingest hook` (rare; hooks call this).
28- kaizen_init — idempotent .kaizen/ + hook patches. kaizen_sync_* — outbox. kaizen_tui — not available (returns JSON stub).
29
30Docs: https://github.com/marquesds/kaizen/blob/main/docs/mcp.md
31"#;
32
33fn ok_str(s: String) -> Result<CallToolResult, ErrorData> {
34 Ok(CallToolResult::success(vec![Content::text(s)]))
35}
36
37fn err_str(msg: String) -> Result<CallToolResult, ErrorData> {
38 Ok(CallToolResult::error(vec![Content::text(msg)]))
39}
40
41async fn run_blocking<T, F>(f: F) -> Result<T, ErrorData>
42where
43 F: FnOnce() -> Result<T, anyhow::Error> + Send + 'static,
44 T: Send + 'static,
45{
46 tokio::task::spawn_blocking(f)
47 .await
48 .map_err(|e| ErrorData::internal_error(e.to_string(), None))?
49 .map_err(|e: anyhow::Error| ErrorData::internal_error(format!("{e:#}"), None))
50}
51
52fn opt_path(ws: &Option<String>) -> Option<std::path::PathBuf> {
53 ws.as_ref().map(std::path::PathBuf::from)
54}
55
56fn resolve_ws(ws: &WorkspaceArg) -> Result<Option<std::path::PathBuf>, ErrorData> {
57 match (ws.workspace.as_deref(), ws.project.as_deref()) {
58 (None, None) => Ok(None),
59 (w, p) => cli::resolve_target(w.map(std::path::Path::new), p)
60 .map(|(path, _)| Some(path))
61 .map_err(|e| ErrorData::internal_error(e.to_string(), None)),
62 }
63}
64
65#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
67struct WorkspaceArg {
68 workspace: Option<String>,
70 project: Option<String>,
72}
73
74#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
76struct WorkspaceJsonArg {
77 workspace: Option<String>,
79 #[serde(default)]
81 all_workspaces: bool,
82 #[serde(default)]
84 json: bool,
85 #[serde(default)]
87 refresh: bool,
88 #[serde(default)]
90 limit: Option<u32>,
91}
92
93#[derive(Debug, Deserialize, schemars::JsonSchema)]
94struct IngestHookArg {
95 #[serde(flatten)]
96 ws: WorkspaceArg,
97 source: String,
99 payload: String,
101}
102
103#[derive(Debug, Deserialize, schemars::JsonSchema)]
104struct SessionIdArg {
105 #[serde(flatten)]
106 ws: WorkspaceArg,
107 id: String,
108}
109
110#[derive(Debug, Deserialize, schemars::JsonSchema)]
111struct GetSpanTreeArg {
112 #[serde(flatten)]
113 ws: WorkspaceArg,
114 id: String,
115 #[serde(default)]
116 json: bool,
117}
118
119#[derive(Debug, Deserialize, schemars::JsonSchema)]
120struct SearchSessionsArg {
121 #[serde(flatten)]
122 ws: WorkspaceArg,
123 query: String,
124 #[serde(default)]
125 since: Option<String>,
126 #[serde(default)]
127 agent: Option<String>,
128 #[serde(default)]
129 kind: Option<String>,
130 #[serde(default = "default_search_limit")]
131 limit: usize,
132}
133
134fn default_search_limit() -> usize {
135 50
136}
137
138#[derive(Debug, Deserialize, schemars::JsonSchema)]
139struct MetricsArg {
140 #[serde(flatten)]
141 ws: WorkspaceArg,
142 #[serde(default)]
143 all_workspaces: bool,
144 #[serde(default = "default_days")]
145 days: u32,
146 json: bool,
148 #[serde(default)]
149 force: bool,
150 #[serde(default)]
152 refresh: bool,
153}
154
155fn default_days() -> u32 {
156 7
157}
158
159#[derive(Debug, Deserialize, schemars::JsonSchema)]
160struct MetricsIndexArg {
161 #[serde(flatten)]
162 ws: WorkspaceArg,
163 #[serde(default)]
164 force: bool,
165}
166
167#[derive(Debug, Deserialize, schemars::JsonSchema)]
168struct SyncRunArg {
169 #[serde(flatten)]
170 ws: WorkspaceArg,
171 #[serde(default = "default_once_true")]
173 once: bool,
174}
175
176fn default_once_true() -> bool {
177 true
178}
179
180#[derive(Debug, Deserialize, schemars::JsonSchema)]
181struct RetroArg {
182 #[serde(flatten)]
183 ws: WorkspaceArg,
184 #[serde(default = "default_days")]
185 days: u32,
186 #[serde(default)]
187 dry_run: bool,
188 #[serde(default)]
189 json: bool,
190 #[serde(default)]
191 force: bool,
192 #[serde(default)]
194 refresh: bool,
195}
196
197#[derive(Debug, Deserialize, schemars::JsonSchema)]
198struct InsightsArg {
199 #[serde(flatten)]
200 ws: WorkspaceArg,
201 #[serde(default)]
202 all_workspaces: bool,
203 #[serde(default)]
205 refresh: bool,
206}
207
208#[derive(Debug, Deserialize, schemars::JsonSchema)]
209struct ExpNewArg {
210 #[serde(flatten)]
211 ws: WorkspaceArg,
212 name: String,
213 hypothesis: String,
214 change: String,
215 metric: String,
216 #[serde(default = "default_bind")]
217 bind: String,
218 #[serde(default = "default_duration")]
219 duration_days: u32,
220 #[serde(default = "default_target")]
221 target_pct: f64,
222 control_commit: Option<String>,
223 treatment_commit: Option<String>,
224 control_branch: Option<String>,
225 treatment_branch: Option<String>,
226}
227
228fn default_bind() -> String {
229 "git".to_string()
230}
231fn default_duration() -> u32 {
232 14
233}
234fn default_target() -> f64 {
235 -10.0
236}
237
238#[derive(Debug, Deserialize, schemars::JsonSchema)]
239struct ExpIdArg {
240 #[serde(flatten)]
241 ws: WorkspaceArg,
242 id: String,
243}
244
245#[derive(Debug, Deserialize, schemars::JsonSchema)]
246struct ExpTagArg {
247 #[serde(flatten)]
248 ws: WorkspaceArg,
249 id: String,
250 session: String,
251 variant: String,
253}
254
255#[derive(Debug, Deserialize, schemars::JsonSchema)]
256struct ExpReportArg {
257 #[serde(flatten)]
258 ws: WorkspaceArg,
259 id: String,
260 #[serde(default)]
261 json: bool,
262 #[serde(default)]
264 refresh: bool,
265}
266
267#[derive(Debug, Deserialize, schemars::JsonSchema)]
268struct AnnotateSessionArg {
269 session_id: String,
271 #[serde(default)]
273 score: Option<u8>,
274 #[serde(default)]
276 label: Option<String>,
277 #[serde(default)]
278 note: Option<String>,
279 #[serde(flatten)]
280 ws: WorkspaceArg,
281}
282
283#[derive(Clone, Debug)]
284pub struct KaizenMcp;
285
286#[tool_router]
287impl KaizenMcp {
288 #[tool(
289 name = "kaizen_capabilities",
290 description = "Read first: when to use summary vs metrics, sessions, retro, and other tools. No DB access; static help text only."
291 )]
292 async fn kaizen_capabilities(
293 &self,
294 Parameters(_): Parameters<WorkspaceArg>,
295 ) -> Result<CallToolResult, ErrorData> {
296 ok_str(MCP_CAPABILITIES.to_string())
297 }
298
299 #[tool(
300 name = "kaizen_ingest_hook",
301 description = "Ingest a hook event (same as `kaizen ingest hook`). Pass payload JSON, not stdin."
302 )]
303 async fn kaizen_ingest_hook(
304 &self,
305 Parameters(IngestHookArg {
306 ws,
307 source,
308 payload,
309 }): Parameters<IngestHookArg>,
310 ) -> Result<CallToolResult, ErrorData> {
311 let src = IngestSource::parse(&source)
312 .ok_or_else(|| ErrorData::invalid_params("source must be cursor or claude", None))?;
313 let w = resolve_ws(&ws)?;
314 run_blocking(move || ingest_hook_string(src, &payload, w)).await?;
315 ok_str(String::new())
316 }
317
318 #[tool(
319 name = "kaizen_sessions_list",
320 description = "List agent sessions in the workspace. Set json=true for structured output. Optional limit caps rows after sort (newest first). Use refresh=true for a full transcript rescan."
321 )]
322 async fn kaizen_sessions_list(
323 &self,
324 Parameters(WorkspaceJsonArg {
325 workspace,
326 all_workspaces,
327 json,
328 refresh,
329 limit,
330 }): Parameters<WorkspaceJsonArg>,
331 ) -> Result<CallToolResult, ErrorData> {
332 let w = opt_path(&workspace);
333 let lim = limit.map(|n| n as usize);
334 let t = run_blocking(move || {
335 cli::sessions_list_text(w.as_deref(), json, refresh, all_workspaces, lim)
336 })
337 .await?;
338 ok_str(t)
339 }
340
341 #[tool(
342 name = "kaizen_session_show",
343 description = "Show one session (kaizen sessions show)"
344 )]
345 async fn kaizen_session_show(
346 &self,
347 Parameters(SessionIdArg { ws, id }): Parameters<SessionIdArg>,
348 ) -> Result<CallToolResult, ErrorData> {
349 let w = resolve_ws(&ws)?;
350 let t = run_blocking(move || cli::session_show_text(&id, w.as_deref())).await?;
351 ok_str(t)
352 }
353
354 #[tool(
355 name = "mcp/search_sessions",
356 description = "BM25 full-text search over session events. Args match `kaizen sessions search`: query, since, agent, kind, limit, workspace."
357 )]
358 async fn search_sessions(
359 &self,
360 Parameters(SearchSessionsArg {
361 ws,
362 query,
363 since,
364 agent,
365 kind,
366 limit,
367 }): Parameters<SearchSessionsArg>,
368 ) -> Result<CallToolResult, ErrorData> {
369 let w = resolve_ws(&ws)?;
370 let (hits, fallback) = run_blocking(move || {
371 crate::shell::search::sessions_search_hits(
372 w.as_deref(),
373 &query,
374 since.as_deref(),
375 agent.as_deref(),
376 kind.as_deref(),
377 limit,
378 )
379 })
380 .await?;
381 Ok(CallToolResult::structured(serde_json::json!({
382 "fallback": fallback,
383 "count": hits.len(),
384 "hits": hits,
385 })))
386 }
387
388 #[tool(
389 name = "kaizen_summary",
390 description = "Roll up session counts, USD cost, top tools, by-agent/model. For **code** hotspots and slow tool p95, use `kaizen_metrics` instead. Set json=true to match `kaizen summary --json` (optional `cost_note` when sessions exist but stored cost rollup is zero)."
391 )]
392 async fn kaizen_summary(
393 &self,
394 Parameters(WorkspaceJsonArg {
395 workspace,
396 all_workspaces,
397 json,
398 refresh,
399 limit: _,
400 }): Parameters<WorkspaceJsonArg>,
401 ) -> Result<CallToolResult, ErrorData> {
402 let w = opt_path(&workspace);
403 let t = run_blocking(move || {
404 cli::summary_text(
405 w.as_deref(),
406 json,
407 refresh,
408 all_workspaces,
409 DataSource::Local,
410 )
411 })
412 .await?;
413 ok_str(t)
414 }
415
416 #[tool(
417 name = "kaizen_tui",
418 description = "Interactive TUI is not available via MCP. Returns guidance."
419 )]
420 async fn kaizen_tui(
421 &self,
422 Parameters(_): Parameters<WorkspaceArg>,
423 ) -> Result<CallToolResult, ErrorData> {
424 Ok(CallToolResult::structured_error(serde_json::json!({
425 "available": false,
426 "reason": "interactive",
427 "cli": "kaizen tui [ --workspace <path> ]"
428 })))
429 }
430
431 #[tool(
432 name = "kaizen_init",
433 description = "Idempotent workspace setup (kaizen init)"
434 )]
435 async fn kaizen_init(
436 &self,
437 Parameters(ws): Parameters<WorkspaceArg>,
438 ) -> Result<CallToolResult, ErrorData> {
439 let w = resolve_ws(&ws)?;
440 let t = run_blocking(move || init::init_text(w.as_deref())).await?;
441 ok_str(t)
442 }
443
444 #[tool(
445 name = "kaizen_insights",
446 description = "Session insights (kaizen insights)"
447 )]
448 async fn kaizen_insights(
449 &self,
450 Parameters(InsightsArg {
451 ws,
452 all_workspaces,
453 refresh,
454 }): Parameters<InsightsArg>,
455 ) -> Result<CallToolResult, ErrorData> {
456 let w = resolve_ws(&ws)?;
457 let t = run_blocking(move || {
458 insights::insights_text(w.as_deref(), all_workspaces, refresh, DataSource::Local)
459 })
460 .await?;
461 ok_str(t)
462 }
463
464 #[tool(
465 name = "kaizen_metrics",
466 description = "Repo + tool intelligence: hottest files, slow tools (p95), token/reasoning sinks, agent pain. Not for simple cost rollups — use `kaizen_summary` first."
467 )]
468 async fn kaizen_metrics(
469 &self,
470 Parameters(MetricsArg {
471 ws,
472 all_workspaces,
473 days,
474 json,
475 force,
476 refresh,
477 }): Parameters<MetricsArg>,
478 ) -> Result<CallToolResult, ErrorData> {
479 let w = resolve_ws(&ws)?;
480 let t = run_blocking(move || {
481 metrics::metrics_text(
482 w.as_deref(),
483 days,
484 json,
485 force,
486 all_workspaces,
487 refresh,
488 DataSource::Local,
489 )
490 })
491 .await?;
492 ok_str(t)
493 }
494
495 #[tool(
496 name = "kaizen_metrics_index",
497 description = "Rebuild repo snapshot index (kaizen metrics index)"
498 )]
499 async fn kaizen_metrics_index(
500 &self,
501 Parameters(MetricsIndexArg { ws, force }): Parameters<MetricsIndexArg>,
502 ) -> Result<CallToolResult, ErrorData> {
503 let w = resolve_ws(&ws)?;
504 let t = run_blocking(move || metrics::metrics_index_text(w.as_deref(), force)).await?;
505 ok_str(t)
506 }
507
508 #[tool(
509 name = "kaizen_sync_run",
510 description = "Flush outbox (kaizen sync run). Use once=true (default). Continuous mode is not supported."
511 )]
512 async fn kaizen_sync_run(
513 &self,
514 Parameters(SyncRunArg { ws, once }): Parameters<SyncRunArg>,
515 ) -> Result<CallToolResult, ErrorData> {
516 if !once {
517 return err_str(
518 "once=false (continuous sync daemon) is not supported over MCP. Run `kaizen sync run` in a shell, or pass once=true (default).".into(),
519 );
520 }
521 let w = resolve_ws(&ws)?;
522 let t = run_blocking(move || sync::sync_run_text(w.as_deref(), true)).await?;
523 ok_str(t)
524 }
525
526 #[tool(
527 name = "kaizen_sync_status",
528 description = "Outbox and sync health (kaizen sync status)"
529 )]
530 async fn kaizen_sync_status(
531 &self,
532 Parameters(ws): Parameters<WorkspaceArg>,
533 ) -> Result<CallToolResult, ErrorData> {
534 let w = resolve_ws(&ws)?;
535 let t = run_blocking(move || sync::sync_status_text(w.as_deref())).await?;
536 ok_str(t)
537 }
538
539 #[tool(
540 name = "kaizen_exp_new",
541 description = "Create experiment (kaizen exp new)"
542 )]
543 async fn kaizen_exp_new(
544 &self,
545 Parameters(ExpNewArg {
546 ws,
547 name,
548 hypothesis,
549 change,
550 metric,
551 bind,
552 duration_days,
553 target_pct,
554 control_commit,
555 treatment_commit,
556 control_branch,
557 treatment_branch,
558 }): Parameters<ExpNewArg>,
559 ) -> Result<CallToolResult, ErrorData> {
560 let w = resolve_ws(&ws)?;
561 let args = NewArgs {
562 name,
563 hypothesis,
564 change,
565 metric,
566 bind,
567 duration_days,
568 target_pct,
569 control_commit,
570 treatment_commit,
571 control_branch,
572 treatment_branch,
573 };
574 let t = run_blocking(move || exp::exp_new_text(w.as_deref(), args)).await?;
575 ok_str(t)
576 }
577
578 #[tool(
579 name = "kaizen_exp_list",
580 description = "List experiments (kaizen exp list)"
581 )]
582 async fn kaizen_exp_list(
583 &self,
584 Parameters(ws): Parameters<WorkspaceArg>,
585 ) -> Result<CallToolResult, ErrorData> {
586 let w = resolve_ws(&ws)?;
587 let t = run_blocking(move || exp::exp_list_text(w.as_deref())).await?;
588 ok_str(t)
589 }
590
591 #[tool(
592 name = "kaizen_exp_status",
593 description = "Show experiment (kaizen exp status)"
594 )]
595 async fn kaizen_exp_status(
596 &self,
597 Parameters(ExpIdArg { ws, id }): Parameters<ExpIdArg>,
598 ) -> Result<CallToolResult, ErrorData> {
599 let w = resolve_ws(&ws)?;
600 let t = run_blocking(move || exp::exp_status_text(w.as_deref(), &id)).await?;
601 ok_str(t)
602 }
603
604 #[tool(
605 name = "kaizen_exp_tag",
606 description = "Tag session variant (kaizen exp tag)"
607 )]
608 async fn kaizen_exp_tag(
609 &self,
610 Parameters(ExpTagArg {
611 ws,
612 id,
613 session,
614 variant,
615 }): Parameters<ExpTagArg>,
616 ) -> Result<CallToolResult, ErrorData> {
617 let w = resolve_ws(&ws)?;
618 let t =
619 run_blocking(move || exp::exp_tag_text(w.as_deref(), &id, &session, &variant)).await?;
620 ok_str(t)
621 }
622
623 #[tool(
624 name = "kaizen_exp_report",
625 description = "Experiment report (kaizen exp report). Optional refresh: true forces a full transcript rescan before computing the report."
626 )]
627 async fn kaizen_exp_report(
628 &self,
629 Parameters(ExpReportArg {
630 ws,
631 id,
632 json,
633 refresh,
634 }): Parameters<ExpReportArg>,
635 ) -> Result<CallToolResult, ErrorData> {
636 let w = resolve_ws(&ws)?;
637 let t =
638 run_blocking(move || exp::exp_report_text(w.as_deref(), &id, json, refresh)).await?;
639 ok_str(t)
640 }
641
642 #[tool(
643 name = "kaizen_exp_conclude",
644 description = "Conclude experiment (kaizen exp conclude)"
645 )]
646 async fn kaizen_exp_conclude(
647 &self,
648 Parameters(ExpIdArg { ws, id }): Parameters<ExpIdArg>,
649 ) -> Result<CallToolResult, ErrorData> {
650 let w = resolve_ws(&ws)?;
651 let t = run_blocking(move || exp::exp_conclude_text(w.as_deref(), &id)).await?;
652 ok_str(t)
653 }
654
655 #[tool(
656 name = "kaizen_exp_start",
657 description = "Start experiment — transition Draft → Running (kaizen exp start)"
658 )]
659 async fn kaizen_exp_start(
660 &self,
661 Parameters(ExpIdArg { ws, id }): Parameters<ExpIdArg>,
662 ) -> Result<CallToolResult, ErrorData> {
663 let w = resolve_ws(&ws)?;
664 let t = run_blocking(move || exp::exp_start_text(w.as_deref(), &id)).await?;
665 ok_str(t)
666 }
667
668 #[tool(
669 name = "kaizen_exp_archive",
670 description = "Archive experiment — transition Concluded → Archived (kaizen exp archive)"
671 )]
672 async fn kaizen_exp_archive(
673 &self,
674 Parameters(ExpIdArg { ws, id }): Parameters<ExpIdArg>,
675 ) -> Result<CallToolResult, ErrorData> {
676 let w = resolve_ws(&ws)?;
677 let t = run_blocking(move || exp::exp_archive_text(w.as_deref(), &id)).await?;
678 ok_str(t)
679 }
680
681 #[tool(
682 name = "kaizen_retro",
683 description = "Heuristic retro report (kaizen retro). Prefer json=true for machine parsing."
684 )]
685 async fn kaizen_retro(
686 &self,
687 Parameters(RetroArg {
688 ws,
689 days,
690 dry_run,
691 json,
692 force,
693 refresh,
694 }): Parameters<RetroArg>,
695 ) -> Result<CallToolResult, ErrorData> {
696 let w = resolve_ws(&ws)?;
697 let t = run_blocking(move || {
698 retro::retro_stdout(
699 w.as_deref(),
700 days,
701 dry_run,
702 json,
703 force,
704 refresh,
705 DataSource::Local,
706 )
707 })
708 .await?;
709 ok_str(t)
710 }
711
712 #[tool(
713 name = "kaizen_annotate_session",
714 description = "Attach human feedback (score 1-5, label, free-text note) to a session."
715 )]
716 async fn kaizen_annotate_session(
717 &self,
718 Parameters(AnnotateSessionArg {
719 session_id,
720 score,
721 label,
722 note,
723 ws,
724 }): Parameters<AnnotateSessionArg>,
725 ) -> Result<CallToolResult, ErrorData> {
726 use crate::feedback::types::FeedbackLabel;
727 let parsed_label = match label.as_deref() {
728 Some(s) => {
729 let l = FeedbackLabel::from_str_opt(s);
730 if l.is_none() {
731 return Err(ErrorData::invalid_params(
732 format!("unknown label: {s}"),
733 None,
734 ));
735 }
736 l
737 }
738 None => None,
739 };
740 let w = resolve_ws(&ws)?;
741 run_blocking(move || {
742 crate::shell::feedback::cmd_sessions_annotate(
743 &session_id,
744 score,
745 parsed_label,
746 note,
747 w.as_deref(),
748 )
749 })
750 .await?;
751 ok_str("annotated".into())
752 }
753
754 #[tool(
755 name = "get_session_span_tree",
756 description = "Return the nested tool-span tree for a session. Each node carries tool name, status, subtree cost, depth, and children. Use json=true for structured output."
757 )]
758 async fn get_session_span_tree(
759 &self,
760 Parameters(GetSpanTreeArg { ws, id, json }): Parameters<GetSpanTreeArg>,
761 ) -> Result<CallToolResult, ErrorData> {
762 let w = resolve_ws(&ws)?;
763 let t = run_blocking(move || {
764 crate::shell::cli::cmd_sessions_tree_text(&id, 999, json, w.as_deref())
765 })
766 .await?;
767 ok_str(t)
768 }
769}
770
771#[tool_handler(
772 name = "kaizen",
773 version = "0.1.0",
774 instructions = "kaizen: local agent telemetry. Call `kaizen_capabilities` first if unsure. Cost/volume: `kaizen_summary`. Code hotspots and slow tools: `kaizen_metrics`. Most CLI workflows are here; shell-only: doctor, guidance, gc, completions, proxy, telemetry. Workspace defaults to the server cwd. `kaizen_tui` is interactive CLI-only. `kaizen_sync_run` supports once=true only."
775)]
776impl ServerHandler for KaizenMcp {}