feat(fb-30): MCP server (stdio) — agent integration MVP #108
Reference in New Issue
Block a user
Delete Branch "feat/p9-fb-30-mcp-server"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
kebab mcp신규 subcommand + new cratekebab-mcp(lib only) — stdio JSON-RPC server. Claude Code / Cursor / OpenAI Agents 등 MCP-aware host 가 native support.search(lexical/vector/hybrid) /ask(RAG, optionalsession_id+modeoverride) /schema(introspection) /doctor(health).tools/list+tools/calldispatch (per-tool 모듈, 명시적, 디버깅 쉬움).변경 사항
feat/p9-fb-30-mcp-server16 commits, 35 files (+1196/-20).새 crate / 모듈
crates/kebab-mcp/(lib only) —KebabHandler(rmcp::ServerHandler),KebabAppState { config, config_path }, 4 tool 모듈 (tools/{schema,doctor,search,ask}.rs),error::{to_tool_success, to_tool_error},serve_stdio(Config, Option<PathBuf>)entry.kebab-app::error_wire— fb-27 의kebab-cli::error_classifypromotion. UI crate 끼리 import 회피 (facade 룰 준수).ErrorV1에schema_version: String필드 추가 — kebab-mcp 의 직접 serialize 경로에서도 wire 정합.CLI 변경
kebab-cli—Cmd::Mcpvariant + arm.serve_stdio(cfg, cli.config.clone())호출.--config <path>honor.동작 결정
Err(e)→isError: true+ error.v1 content. Refusal / no-hit / unhealthy 는 정상 응답 (semantic flag 으로 agent 가 분기 — MCP 표준 패턴).tokio::task::spawn_blockingwrap —OllamaLanguageModel의reqwest::blocking::Client가 async 안에서 panic 회피. schema / doctor 는 cheap reads, 비-wrap.Capabilities::mcp_serverfalse→true.신규 wire / surface
테스트
cargo clippy --workspace --all-targets -- -D warningsclean.cargo test --workspace -j 1— 2 reset.rs failure 는 pre-existing env-dependent (XDG_CONFIG_HOME), main 에서도 동일 — fb-30 무관.target/debug/kebab mcpspawn + JSON-RPC initialize + tools/list) 정상.알려진 한계 (deferred)
tasks/HOTFIXES.md의2026-05-07 — p9-fb-30항목이 source of truth.ask— fb-33 streaming ask 와 함께.ingest_*/fetch/list_docs/inspect_chunktools — 후속 task.KebabAppState에OnceLock<SqliteStore>도입은 후속 PR.Spec contract
tasks/p9/p9-fb-30-mcp-server.mdstatusopen→completed. depends_on 이미[p9-fb-27](fb-29 deferred 시 갱신됨).transport: stdio default + http (fb-29 daemon) 위에 SSE 옵션→ 실제 stdio 단일 (fb-29 deferral 결과).Release trigger
머지 후 별 PR — 0.3.0 → 0.4.0 minor bump +
gitea-release v0.4.0. fb-27 precedent (PR #105) 와 동일 패턴 —chore/bump-v0.4.0branch + PR + tag.trigger 3 충족: (1) 신규 CLI surface (
kebab mcp), (2) new crate (kebab-mcp), (3) capability flag flip + design §10.2 변경.agent integration "MVP" 완성 신호 — release notes 에 강조: "MCP 표준 protocol 으로 Claude Code / Cursor / OpenAI Agents 등 host-agnostic 사용 가능".
Two issues from Task 7 review: 1. CRITICAL — call_tool "ask" arm called blocking ask handle from async context. OllamaLanguageModel::new builds reqwest::blocking::Client which creates+drops a tokio runtime → panic inside async. Fix: tokio::task::spawn_blocking wrap. Also applied preemptively to "search" arm (SqliteStore + Lance open are blocking IO too). 2. IMPORTANT — ask tool's retrieval mode hardcoded to Lexical (test workaround for provider="none"); CLI default is Hybrid. Fix: add `mode: Option<String>` field to AskInput, default Hybrid in handle, test passes mode=Some("lexical") explicitly to keep test functional on provider="none". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 1 — fb-30 전반 단단. 16 commit story 직조 깔끔 (scaffold → 4 tool TDD → fixup → CLI wiring → tests → doc → status flip), facade 룰 / wire 정합 / spawn_blocking 배치 모두 정확. spec → impl 매핑 손실 없음.
다음 5 nit/follow-up 정리 후 회차 2 가능:
error_wire.rs의"error.v1".to_string()9 site 반복 —ERROR_V1_IDconst 도입 (schema.rs 의 SCHEMA_V1_ID 와 동일 패턴).lib.rs의"search"+"ask"arm 의 spawn_blocking + JoinError 맵핑 boilerplate 중복 —spawn_toolhelper 로 묶기 (5 번째 tool 추가 시점부터 가치 큼).ask.rs의AskOpts { ... 9 fields ... }— Default 미도입의 그림자. HOTFIXES Known-limitation 한 줄 추가 또는 kebab-app 에 impl Default 별 PR.ask.rs의serde_json::to_value(&answer).unwrap_or_default()— 실패 시 silent null. tool_error fallthrough 권장.ARCHITECTURE.md디렉토리 트리의tools: search, ask, doctor—schema누락 (v1 surface 는 4 tool).테스트 커버리지 양호 (8 신규), clippy clean, manual smoke (initialize + tools/list) 정상. agent integration MVP 으로 적합. 0.4.0 minor cut trigger 3 모두 충족 (신규 surface + new crate + capability flip + design §10.2).
@@ -0,0 +23,4 @@pub fn classify(err: &anyhow::Error, verbose: bool) -> ErrorV1 {if let Some(s) = err.downcast_ref::<ConfigInvalid>() {return ErrorV1 {schema_version: "error.v1".to_string(),[DRY]
schema_version: "error.v1".to_string()9 군데 반복 (line 26 / 38 / 53 / 66 / 77 / 87 / 94 / 101 / 108).kebab-app::SCHEMA_V1_ID패턴 (schema.rs 의pub const SCHEMA_V1_ID: &str = "schema.v1") 동일하게pub const ERROR_V1_ID: &str = "error.v1"도입 + 9 사이트 모두schema_version: ERROR_V1_ID.to_string()로 교체 권장.향후
error.v2bump 시 한 곳만 변경. 또 docs/wire-schema/v1/error.schema.json 의"const": "error.v1"와 grep 으로 묶어 추적 가능. nit 이지만 cascade 규약 정신과 같은 결.@@ -0,0 +107,4 @@}};let state = self.state.clone();let result = tokio::task::spawn_blocking(move || {[중복]
"search"arm (line 105-115 인근) 과"ask"arm (line 124-134 인근) 가 거의 동일한 spawn_blocking + JoinError-mapping boilerplate ~17 line 씩 반복. argument 파싱 + spawn + map_err 까지 동일 shape — 한 helper 로 묶어 두 site 가 한 줄 dispatch 만:그러면 두 arm 이 한 줄 —
"search" => self.spawn_tool(args, tools::search::handle).await,. 5 번째 tool 추가 시점부터는 더 큰 가치. 본 PR 머지 차단 아님 — follow-up 으로 OK.@@ -0,0 +28,4 @@Some("vector") => kebab_core::SearchMode::Vector,_ => kebab_core::SearchMode::Hybrid, // default + "hybrid" + unknown};let opts = kebab_app::AskOpts {[fragile]
kebab_app::AskOpts { ... 9 fields ... }명시적 9 필드 초기화 —AskOpts에 새 field (예:top_p같은 LLM 파라미터) 추가 시 본 site 도 즉시 갱신 안 하면 컴파일 실패. 명시적 컴파일러 신호라 안전하지만, kebab-cli 의Cmd::Askarm + kebab-tui 의 ask 로직 + 본 site 가 모두 같은 모양 — 사실상AskOpts::default()가 있어야 할 자리.kebab-app 에
impl Default for AskOpts도입은 별 task — 본 PR scope 아니지만 HOTFIXES 의 "Known limitation" 절에 한 줄 추가 권장:또는 kebab-app 안에서
AskOpts::default_for_mcp()factory 같은 helper 도 옵션.@@ -0,0 +48,4 @@};match result {Ok(answer) => {// `Answer` does not carry `schema_version`; tag inline (idempotent[silent failure 위험]
serde_json::to_value(&answer).unwrap_or_default()가 실패하면Value::Null반환 — 다음 줄의if let Value::Object패턴 매치 실패로 schema_version 태그 안 됨, 결과적으로 agent 가 빈 JSONnull을 받음. fallthrough error message 없이 silent.Answerserialize 가 실패할 시나리오는 거의 없지만 (POD 구조체), 방어로:그 다음 if let Value::Object 분기 + to_string. 한 single-shot match 가 두 번 (to_value + to_string) 일어나는 게 거추장스러우면 무시 OK — 현 unwrap_or_default 도 실용상 작동.
@@ -168,6 +170,7 @@ kebab/│ ├── kebab-parse-pdf/ # lopdf per-page text extractor (P7-1)│ ├── kebab-app/ # facade (P0 시그니처 + P3-5/P6-4/P7-3 본체)[누락]
tools: search, ask, doctor—schema빠짐. v1 surface 가 4 tool (schema/doctor/search/ask). 한 자 추가:회차 2 — 회차 1 의 5 nit 모두 정확 반영. 추가 actionable 없음.
확인:
ERROR_V1_IDconst 가error_wire.rs에 신설 + lib.rs re-export + 9 inline literal 모두 const 사용 (single source of truth, schema.rs 의 SCHEMA_V1_ID 와 동일 패턴).KebabHandler::spawn_tool<I, F>helper 추출 — search + ask arm 이 ~17 line → 5 line. closure pattern (state by value + handle takes &state) 가 기존tools::*::handlesignature 변경 없이 helper 통합. 5 번째 tool 추가 시 boilerplate 안 늘어남.ask.rs의to_value(&answer)defensive — 실패 시 silent Null 위험 제거, 명시적 to_tool_error fallthrough.AskOptsDefault 미도입 항목 한 줄 추가.ARCHITECTURE.md디렉토리 트리의kebab-mcp항목에schema추가 — v1 surface 4 tool 모두 명시.테스트 모두 통과 (kebab-app error_wire 7/7, kebab-mcp all, kebab-cli 4/4). clippy --workspace clean. agent integration MVP 머지 가능.