feat(fb-30): MCP server (stdio) — agent integration MVP #108

Merged
altair823 merged 17 commits from feat/p9-fb-30-mcp-server into main 2026-05-07 08:04:55 +00:00
Owner

요약

  • kebab mcp 신규 subcommand + new crate kebab-mcp (lib only) — stdio JSON-RPC server. Claude Code / Cursor / OpenAI Agents 등 MCP-aware host 가 native support.
  • 4 read-only tool: search (lexical/vector/hybrid) / ask (RAG, optional session_id + mode override) / schema (introspection) / doctor (health).
  • rmcp 1.6 SDK 채택. manual tools/list + tools/call dispatch (per-tool 모듈, 명시적, 디버깅 쉬움).
  • agent integration "MVP" 완성 — fb-27 capability matrix + fb-30 stdio MCP 가 0.4.0 release 의 핵심.

변경 사항

feat/p9-fb-30-mcp-server 16 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_classify promotion. UI crate 끼리 import 회피 (facade 룰 준수). ErrorV1schema_version: String 필드 추가 — kebab-mcp 의 직접 serialize 경로에서도 wire 정합.

CLI 변경

  • kebab-cliCmd::Mcp variant + arm. serve_stdio(cfg, cli.config.clone()) 호출. --config <path> honor.

동작 결정

  • Tool dispatch error mapping: Err(e)isError: true + error.v1 content. Refusal / no-hit / unhealthy 는 정상 응답 (semantic flag 으로 agent 가 분기 — MCP 표준 패턴).
  • Concurrency: ask + search call_tool arm 이 tokio::task::spawn_blocking wrap — OllamaLanguageModelreqwest::blocking::Client 가 async 안에서 panic 회피. schema / doctor 는 cheap reads, 비-wrap.
  • Capability flag flip: Capabilities::mcp_server falsetrue.

신규 wire / surface

  • design §10.2 MCP transport 절 신설.
  • README 명령 표 + MCP usage section + Claude Code mcp.json 예시.
  • integrations/claude-code/kebab/SKILL.md — MCP 사용 권장 + config 예시.

테스트

  • 신규 ~8 테스트 — kebab-mcp integration (initialize / tools_list / tools_call_{schema,doctor,search,ask} / error_mapping) + kebab-cli (cli_mcp_smoke).
  • 모든 fb-30 테스트 PASS.
  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • cargo test --workspace -j 1 — 2 reset.rs failure 는 pre-existing env-dependent (XDG_CONFIG_HOME), main 에서도 동일 — fb-30 무관.
  • Manual smoke (target/debug/kebab mcp spawn + JSON-RPC initialize + tools/list) 정상.

알려진 한계 (deferred)

tasks/HOTFIXES.md2026-05-07 — p9-fb-30 항목이 source of truth.

  • HTTP-SSE transport — fb-29 P+ deferral 따라 stdio 단일. browser agent / remote 시���리오 등장 시 재개.
  • Resources / Prompts — defer (fb-35 verbatim fetch + RAG prompt 내장 으로 가치 약함).
  • Streaming ask — fb-33 streaming ask 와 함께.
  • ingest_* / fetch / list_docs / inspect_chunk tools — 후속 task.
  • Server-scope state caching — 매 tool call 마다 store open. KebabAppStateOnceLock<SqliteStore> 도입은 후속 PR.
  • Manual dispatch tax — 새 tool 추가 시 두 곳 (list_tools vec + call_tool match) 갱신. 5+ tool 시 매크로 재검토.
  • rmcp SDK API — 1.6 채택. major bump 시 별 task.

Spec contract

  • design §10.2 (MCP transport) 추가.
  • task spec tasks/p9/p9-fb-30-mcp-server.md status opencompleted. depends_on 이미 [p9-fb-27] (fb-29 deferred 시 갱신됨).
  • 본 spec stub 의 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.0 branch + 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 사용 가능".

## 요약 - **`kebab mcp` 신규 subcommand** + new crate `kebab-mcp` (lib only) — stdio JSON-RPC server. Claude Code / Cursor / OpenAI Agents 등 MCP-aware host 가 native support. - **4 read-only tool**: `search` (lexical/vector/hybrid) / `ask` (RAG, optional `session_id` + `mode` override) / `schema` (introspection) / `doctor` (health). - **rmcp 1.6 SDK** 채택. manual `tools/list` + `tools/call` dispatch (per-tool 모듈, 명시적, 디버깅 쉬움). - agent integration "MVP" 완성 — fb-27 capability matrix + fb-30 stdio MCP 가 0.4.0 release 의 핵심. ## 변경 사항 `feat/p9-fb-30-mcp-server` 16 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_classify` promotion. UI crate 끼리 import 회피 (facade 룰 준수). `ErrorV1` 에 `schema_version: String` 필드 추가 — kebab-mcp 의 직접 serialize 경로에서도 wire 정합. ### CLI 변경 - **`kebab-cli`** — `Cmd::Mcp` variant + arm. `serve_stdio(cfg, cli.config.clone())` 호출. `--config <path>` honor. ### 동작 결정 - **Tool dispatch error mapping**: `Err(e)` → `isError: true` + error.v1 content. **Refusal / no-hit / unhealthy 는 정상 응답** (semantic flag 으로 agent 가 분기 — MCP 표준 패턴). - **Concurrency**: ask + search call_tool arm 이 `tokio::task::spawn_blocking` wrap — `OllamaLanguageModel` 의 `reqwest::blocking::Client` 가 async 안에서 panic 회피. schema / doctor 는 cheap reads, 비-wrap. - **Capability flag flip**: `Capabilities::mcp_server` `false` → `true`. ### 신규 wire / surface - design §10.2 MCP transport 절 신설. - README 명령 표 + MCP usage section + Claude Code mcp.json 예시. - integrations/claude-code/kebab/SKILL.md — MCP 사용 권장 + config 예시. ## 테스트 - 신규 ~8 테스트 — kebab-mcp integration (initialize / tools_list / tools_call_{schema,doctor,search,ask} / error_mapping) + kebab-cli (cli_mcp_smoke). - 모든 fb-30 테스트 PASS. - `cargo clippy --workspace --all-targets -- -D warnings` clean. - `cargo test --workspace -j 1` — 2 reset.rs failure 는 pre-existing env-dependent (XDG_CONFIG_HOME), main 에서도 동일 — fb-30 무관. - Manual smoke (`target/debug/kebab mcp` spawn + JSON-RPC initialize + tools/list) 정상. ## 알려진 한계 (deferred) `tasks/HOTFIXES.md` 의 `2026-05-07 — p9-fb-30` 항목이 source of truth. - **HTTP-SSE transport** — fb-29 P+ deferral 따라 stdio 단일. browser agent / remote 시���리오 등장 시 재개. - **Resources / Prompts** — defer (fb-35 verbatim fetch + RAG prompt 내장 으로 가치 약함). - **Streaming `ask`** — fb-33 streaming ask 와 함께. - **`ingest_*` / `fetch` / `list_docs` / `inspect_chunk` tools** — 후속 task. - **Server-scope state caching** — 매 tool call 마다 store open. `KebabAppState` 에 `OnceLock<SqliteStore>` 도입은 후속 PR. - **Manual dispatch tax** — 새 tool 추가 시 두 곳 (list_tools vec + call_tool match) 갱신. 5+ tool 시 매크로 재검토. - **rmcp SDK API** — 1.6 채택. major bump 시 별 task. ## Spec contract - design §10.2 (MCP transport) 추가. - task spec `tasks/p9/p9-fb-30-mcp-server.md` status `open` → `completed`. depends_on 이미 `[p9-fb-27]` (fb-29 deferred 시 갱신됨). - 본 spec stub 의 `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.0` branch + 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 사용 가능".
altair823 added 16 commits 2026-05-07 07:52:51 +00:00
fb-30 의 새 crate `kebab-mcp` 가 동일 classify 모듈 사용 — UI crate 끼리
import 는 facade rule 위반이므로 kebab-app 으로 promotion. fb-27 commit
c91228e 의 코드 그대로 이전 (struct + classify + classify_llm + 7 unit
test). reqwest dev-dep 도 함께 이동.

kebab-cli 는 `kebab_app::ErrorV1` / `kebab_app::classify` 로 import 경로
1줄 변경 + wire.rs 의 `&crate::error_classify::ErrorV1` 1줄 교체. 동작
무영향.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empty lib + serve_stdio entry that bails until Task 3 wires rmcp. Adds
rmcp 1.6 to workspace dependencies (server + macros + transport-io +
schemars features) + tokio multi-thread/io-util/io-std local extensions.

schemars declared as "1" (resolved to 1.2.1) — matches rmcp 1.6's ^1.0
requirement (verified via crates.io /dependencies; plan literal was 0.9
which would conflict). Path-style refs for kebab-app / kebab-config /
kebab-core follow workspace convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KebabHandler implements rmcp::ServerHandler::get_info — returns
serverInfo (name="kebab", version from CARGO_PKG_VERSION) and
capabilities.tools. KebabAppState wraps Config in Arc for cheap clone
into per-request task scope. serve_stdio entry builds a multi-thread
tokio runtime and runs the server until client closes the stream.

rmcp 1.6 API used:
- rmcp::ServerHandler trait (re-exported from handler::server)
- ServerInfo::new(caps).with_server_info(impl) builder (not struct-init:
  InitializeResult/Implementation are #[non_exhaustive])
- ServerCapabilities::builder().enable_tools().build() — builder macro
  generated, confirms the plan-literal pattern works
- Implementation::new(name, version) — non-exhaustive constructor
- rmcp::transport::stdio() returns (tokio::io::Stdin, tokio::io::Stdout)
  tuple; tuple impls IntoTransport via AsyncRead+AsyncWrite blanket
- handler.serve(transport).await → RunningService<RoleServer, H>
  (ServiceExt::serve, returns Result<_, ServerInitializeError>)
- service.waiting().await → Result<QuitReason, JoinError>
- serve_stdio is plain fn wrapping a manually-built tokio runtime
  (avoids nested-runtime hazard if kebab-cli ever gains its own rt)

Tools wire-up lands in subsequent tasks (one tool per task).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First tool wired — `schema` (no input args, returns schema.v1 JSON
mirroring `kebab schema --json`). Establishes the per-tool module
pattern (crates/kebab-mcp/src/tools/<name>.rs) + error helper that maps
anyhow::Error to MCP CallToolResult.error with error.v1 content.

Dispatch pattern: manual dispatch — explicit `list_tools` + `call_tool`
overrides on `impl ServerHandler for KebabHandler` with a
`match request.name.as_ref()` arm per tool. No proc-macro magic.
Tasks 5-7 should add a new arm + new tools/<name>.rs following the same
pattern; also add a `Tool::new(...)` entry in `list_tools`.

API shapes confirmed from rmcp 1.6 source:
- Content = Annotated<RawContent>; text via `Content::text(s)`; pattern
  match via `&content.raw` → `RawContent::Text(t)` → `t.text`
- CallToolResult::success(Vec<Content>) / ::error(Vec<Content>)
- ListToolsResult::with_all_items(Vec<Tool>)
- schema_for_empty_input() from rmcp::handler::server::common

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second tool — `doctor` (no input args, returns doctor.v1 JSON via
kebab_app::doctor_with_config_path). Mirrors schema tool's manual-dispatch
pattern: Tool::new entry in list_tools, match arm in call_tool, per-tool
module in tools/doctor.rs.

doctor_with_config_path takes Option<&Path> (not &Config), so KebabAppState
is extended with config_path: Option<PathBuf>. All existing callers
(initialize.rs, tools_call_schema.rs, serve_stdio_async) pass None for now;
Plan Task 10 (Cmd::Mcp wiring) will thread the actual --config path through.
doctor_with_config falls back to XDG default when config_path is None —
same behavior as bare `kebab doctor`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third tool — `search` (input: query / mode / k). First tool with
non-empty input — establishes the pattern: SearchInput struct with
JsonSchema derive + Tool::new uses
rmcp::handler::server::common::schema_for_type::<SearchInput>() for
inputSchema + call_tool match arm parses request.arguments via
serde_json::from_value.

search_with_config takes owned Config, so state.config (Arc<Config>)
is cloned via (*state.config).clone(). Output: search_hit.v1 array —
SearchHit (kebab-core) does not carry schema_version field, so each
element is tagged inline before serialising.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourth (and final v1) tool — `ask` (input: query / optional session_id).
Multi-turn via optional session_id (kebab_app::ask_with_session_with_config),
single-shot via ask_with_config when None. Refusal (grounded:false) NOT
mapped to isError — agent branches on the wire payload's grounded flag.

AskOpts has no Default impl (must construct manually). Answer carries no
schema_version field (tagged inline via entry().or_insert_with, idempotent).
Mode defaulted to Lexical: reqwest::blocking::Client::build creates and
drops a tokio runtime, panicking inside async context — the empty-corpus
refusal test avoids this via spawn_blocking; the tool itself uses Lexical
as the default mode since MCP callers typically run without an embedding
provider configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Adds integration test schema_tool_emits_error_v1_when_db_missing that
verifies NotIndexed errors are emitted as error.v1 JSON with isError=true.
Also fixes ErrorV1 struct to include required schema_version field per
error.v1 wire contract (docs/wire-schema/v1/error.schema.json).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 8 commit f9a1548 added `schema_version: String` as required field on
ErrorV1 (so kebab-mcp's direct serialize-then-emit path produces correct
error.v1 wire). The wire.rs ErrorV1 literal in the
error_wrapper_tags_schema_version_and_emits_code test was missed —
breaks kebab-cli build. Add the field to the test fixture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Approach: extracted `pub fn build_tools_vec() -> Vec<Tool>` from the
inline `list_tools` trait impl body — `RequestContext<RoleServer>` is
non-constructible from outside rmcp (Peer::new is pub(crate)), so a
direct trait-method call was not viable without an in-memory transport
rmcp 1.6 does not expose. The helper is the single source of truth;
`list_tools` now delegates to it.

Three test cases in tests/tools_list.rs:
- 4 tools present with correct names
- search inputSchema has "required": ["query"]
- schema/doctor tools accept empty input (type=object, no required)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires kebab_mcp::serve_stdio into kebab-cli. `--config <path>` honored
via the established Config::load pattern.

Updated serve_stdio signature to (Config, Option<PathBuf>) so the doctor
tool's path-aware behavior works correctly via KebabAppState.

Smoke test spawns the binary + sends initialize + initialized +
tools/list over stdin, asserts 4 tools returned. Confirms the MCP
server boots end-to-end via the real binary (rmcp 1.6 has no
in-memory test transport, so this is the only end-to-end assertion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README 명령 표 에 `kebab mcp` 추가 + Claude Code MCP config 예시
- HANDOFF post-도그푸딩 항목 한 줄 (rmcp 1.6 + manual dispatch + error_wire promotion + ask/search spawn_blocking + capability flag flip 명시)
- CLAUDE.md facade 룰 의 UI crate 카테고리 에 `kebab-mcp` 추가
- integrations skill — MCP 사용 안내 (recommended over subprocess)
- design §10.2 MCP transport 절 신설

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final review fixes:

- docs/ARCHITECTURE.md: add kebab-mcp to UI subgraph + directory tree
  (CLAUDE.md "add new crate" rule required this; missed in Task 12 doc sync).
- state.rs: replace forward-reference Task 10 comment with current-state
  doc (config_path now wired by Task 10 commit 4a30959).
- tools_call_schema.rs: assert capabilities.mcp_server == true (already
  pinned in schema_report + cli_schema, this closes the gap in mcp's own
  test).

Version bump 0.3.0 → 0.4.0 deferred to separate `chore/bump-v0.4.0` PR
mirroring fb-27 precedent (commit 73f5d73 / PR #105).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-07 07:55:34 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 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 가능:

  1. error_wire.rs"error.v1".to_string() 9 site 반복 — ERROR_V1_ID const 도입 (schema.rs 의 SCHEMA_V1_ID 와 동일 패턴).
  2. lib.rs"search" + "ask" arm 의 spawn_blocking + JoinError 맵핑 boilerplate 중복 — spawn_tool helper 로 묶기 (5 번째 tool 추가 시점부터 가치 큼).
  3. ask.rsAskOpts { ... 9 fields ... } — Default 미도입의 그림자. HOTFIXES Known-limitation 한 줄 추가 또는 kebab-app 에 impl Default 별 PR.
  4. ask.rsserde_json::to_value(&answer).unwrap_or_default() — 실패 시 silent null. tool_error fallthrough 권장.
  5. ARCHITECTURE.md 디렉토리 트리의 tools: search, ask, doctorschema 누락 (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).

회차 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 가능: 1. `error_wire.rs` 의 `"error.v1".to_string()` 9 site 반복 — `ERROR_V1_ID` const 도입 (schema.rs 의 SCHEMA_V1_ID 와 동일 패턴). 2. `lib.rs` 의 `"search"` + `"ask"` arm 의 spawn_blocking + JoinError 맵핑 boilerplate 중복 — `spawn_tool` helper 로 묶기 (5 번째 tool 추가 시점부터 가치 큼). 3. `ask.rs` 의 `AskOpts { ... 9 fields ... }` — Default 미도입의 그림자. HOTFIXES Known-limitation 한 줄 추가 또는 kebab-app 에 impl Default 별 PR. 4. `ask.rs` 의 `serde_json::to_value(&answer).unwrap_or_default()` — 실패 시 silent null. tool_error fallthrough 권장. 5. `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.v2 bump 시 한 곳만 변경. 또 docs/wire-schema/v1/error.schema.json 의 "const": "error.v1" 와 grep 으로 묶어 추적 가능. nit 이지만 cascade 규약 정신과 같은 결.

**[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.v2` bump 시 한 곳만 변경. 또 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 만:

async fn spawn_tool<I, F>(
    &self,
    args: serde_json::Map<String, serde_json::Value>,
    handle: F,
) -> Result<CallToolResult, ErrorData>
where
    I: serde::de::DeserializeOwned + Send + 'static,
    F: FnOnce(&KebabAppState, I) -> CallToolResult + Send + 'static,
{
    let input: I = match serde_json::from_value(serde_json::Value::Object(args)) {
        Ok(i) => i,
        Err(e) => return Ok(error::to_tool_error(&anyhow::Error::from(e))),
    };
    let state = self.state.clone();
    tokio::task::spawn_blocking(move || handle(&state, input))
        .await
        .map_err(|e| ErrorData::internal_error(e.to_string(), None))
}

그러면 두 arm 이 한 줄 — "search" => self.spawn_tool(args, tools::search::handle).await,. 5 번째 tool 추가 시점부터는 더 큰 가치. 본 PR 머지 차단 아님 — follow-up 으로 OK.

**[중복]** `"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 만: ```rust async fn spawn_tool<I, F>( &self, args: serde_json::Map<String, serde_json::Value>, handle: F, ) -> Result<CallToolResult, ErrorData> where I: serde::de::DeserializeOwned + Send + 'static, F: FnOnce(&KebabAppState, I) -> CallToolResult + Send + 'static, { let input: I = match serde_json::from_value(serde_json::Value::Object(args)) { Ok(i) => i, Err(e) => return Ok(error::to_tool_error(&anyhow::Error::from(e))), }; let state = self.state.clone(); tokio::task::spawn_blocking(move || handle(&state, input)) .await .map_err(|e| ErrorData::internal_error(e.to_string(), None)) } ``` 그러면 두 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::Ask arm + kebab-tui 의 ask 로직 + 본 site 가 모두 같은 모양 — 사실상 AskOpts::default() 가 있어야 할 자리.

kebab-app 에 impl Default for AskOpts 도입은 별 task — 본 PR scope 아니지만 HOTFIXES 의 "Known limitation" 절에 한 줄 추가 권장:

AskOpts 가 Default 미도입 — 새 field 추가 시 kebab-cli + kebab-tui + kebab-mcp 의 모든 호출 site 동시 갱신. impl Default 도입은 별 PR.

또는 kebab-app 안에서 AskOpts::default_for_mcp() factory 같은 helper 도 옵션.

**[fragile]** `kebab_app::AskOpts { ... 9 fields ... }` 명시적 9 필드 초기화 — `AskOpts` 에 새 field (예: `top_p` 같은 LLM 파라미터) 추가 시 본 site 도 즉시 갱신 안 하면 컴파일 실패. 명시적 컴파일러 신호라 안전하지만, kebab-cli 의 `Cmd::Ask` arm + kebab-tui 의 ask 로직 + 본 site 가 모두 같은 모양 — 사실상 `AskOpts::default()` 가 있어야 할 자리. kebab-app 에 `impl Default for AskOpts` 도입은 별 task — 본 PR scope 아니지만 HOTFIXES 의 "Known limitation" 절에 한 줄 추가 권장: > AskOpts 가 Default 미도입 — 새 field 추가 시 kebab-cli + kebab-tui + kebab-mcp 의 모든 호출 site 동시 갱신. impl Default 도입은 별 PR. 또는 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 가 빈 JSON null 을 받음. fallthrough error message 없이 silent.

Answer serialize 가 실패할 시나리오는 거의 없지만 (POD 구조체), 방어로:

let mut v = match serde_json::to_value(&answer) {
    Ok(v) => v,
    Err(e) => return to_tool_error(&anyhow::anyhow!("answer serialize failed: {e}")),
};

그 다음 if let Value::Object 분기 + to_string. 한 single-shot match 가 두 번 (to_value + to_string) 일어나는 게 거추장스러우면 무시 OK — 현 unwrap_or_default 도 실용상 작동.

**[silent failure 위험]** `serde_json::to_value(&answer).unwrap_or_default()` 가 실패하면 `Value::Null` 반환 — 다음 줄의 `if let Value::Object` 패턴 매치 실패로 schema_version 태그 안 됨, 결과적으로 agent 가 빈 JSON `null` 을 받음. fallthrough error message 없이 silent. `Answer` serialize 가 실패할 시나리오는 거의 없지만 (POD 구조체), 방어로: ```rust let mut v = match serde_json::to_value(&answer) { Ok(v) => v, Err(e) => return to_tool_error(&anyhow::anyhow!("answer serialize failed: {e}")), }; ``` 그 다음 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, doctorschema 빠짐. v1 surface 가 4 tool (schema / doctor / search / ask). 한 자 추가:

│   ├── kebab-mcp/                                     # stdio MCP server — tools: schema, doctor, search, ask (P9-FB-30)
**[누락]** `tools: search, ask, doctor` — `schema` 빠짐. v1 surface 가 4 tool (`schema` / `doctor` / `search` / `ask`). 한 자 추가: ``` │ ├── kebab-mcp/ # stdio MCP server — tools: schema, doctor, search, ask (P9-FB-30) ```
altair823 added 1 commit 2026-05-07 07:59:49 +00:00
- error_wire.rs: extract `pub const ERROR_V1_ID = "error.v1"` + replace
  9 inline literals (parallel to schema.rs::SCHEMA_V1_ID pattern).
  Re-export via kebab-app::lib.rs.
- kebab-mcp/src/lib.rs: extract `KebabHandler::spawn_tool<I, F>` helper —
  search + ask arms reduce from ~17 lines each to a one-line dispatch.
  Future tool 추가 시 boilerplate 안 늘림.
- ask.rs: defensive `to_value(&answer)` — silent Null 위험 제거, 실패
  시 to_tool_error fallthrough.
- HOTFIXES: note AskOpts Default 미도입 limitation.
- ARCHITECTURE.md: directory tree 의 kebab-mcp 항목에 `schema` 추가
  (4 tool 모두 명시).

Round 1 review summary: #108 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-07 08:00:36 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 의 5 nit 모두 정확 반영. 추가 actionable 없음.

확인:

  1. ERROR_V1_ID const 가 error_wire.rs 에 신설 + lib.rs re-export + 9 inline literal 모두 const 사용 (single source of truth, schema.rs 의 SCHEMA_V1_ID 와 동일 패턴).
  2. KebabHandler::spawn_tool<I, F> helper 추출 — search + ask arm 이 ~17 line → 5 line. closure pattern (state by value + handle takes &state) 가 기존 tools::*::handle signature 변경 없이 helper 통합. 5 번째 tool 추가 시 boilerplate 안 늘어남.
  3. ask.rsto_value(&answer) defensive — 실패 시 silent Null 위험 제거, 명시적 to_tool_error fallthrough.
  4. HOTFIXES 의 fb-30 known-limitation 절 마지막에 AskOpts Default 미도입 항목 한 줄 추가.
  5. 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 머지 가능.

회차 2 — 회차 1 의 5 nit 모두 정확 반영. 추가 actionable 없음. 확인: 1. `ERROR_V1_ID` const 가 `error_wire.rs` 에 신설 + lib.rs re-export + 9 inline literal 모두 const 사용 (single source of truth, schema.rs 의 SCHEMA_V1_ID 와 동일 패턴). 2. `KebabHandler::spawn_tool<I, F>` helper 추출 — search + ask arm 이 ~17 line → 5 line. closure pattern (state by value + handle takes &state) 가 기존 `tools::*::handle` signature 변경 없이 helper 통합. 5 번째 tool 추가 시 boilerplate 안 늘어남. 3. `ask.rs` 의 `to_value(&answer)` defensive — 실패 시 silent Null 위험 제거, 명시적 to_tool_error fallthrough. 4. HOTFIXES 의 fb-30 known-limitation 절 마지막에 `AskOpts` Default 미도입 항목 한 줄 추가. 5. `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 머지 가능.
altair823 merged commit eb4f594dda into main 2026-05-07 08:04:55 +00:00
altair823 deleted branch feat/p9-fb-30-mcp-server 2026-05-07 08:04:57 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#108