# ADR-0005: In-app tool calling architecture (MCP-equivalent) > **상태**: Accepted > **작성**: [AI] Architect · **일자**: 2026-06-15 > **추적성** — Redmine #260, 설계서 `docs/design/260-gemma-tool-calling/README.md`, ADR-0003 (on-device Gemma 채택) ## 컨텍스트 #218 로 on-device Gemma 4 추론이 동작하고 #226 으로 카탈로그 47 항목이 노출됐다. 사용자가 "이거 내 습관으로 추가해줘" 같은 자연어 요청으로 DB mutation 까지 도달하는 경로가 필요하다. 선택지: 1. **별도 MCP 서버 띄우기** — 표준 프로토콜, 외부 process. 모바일 환경에서 IPC + 추가 메모리 부담 + 모델 컨텍스트 비용 동일. 2. **In-process Dart 함수 직접 호출** — 같은 프로세스 안에서 tool 정의 + 핸들러. flutter_gemma 0.16.5 의 `tools` 파라미터 위에 얇은 라우터. 3. **Prompt engineering 으로 mutation 안내** — 모델이 "다음과 같이 하세요" 텍스트로만 응답, 실제 액션은 사용자가 수동 — UX 후퇴, R 규칙 enforce 불가. 추가로 결정해야 할 부수 항목: - R1~R10 운영 규칙을 **어디서** enforce 할 것인가 - tool schema 의 source-of-truth (Dart 코드 vs yaml/json 파일) - destructive tool 의 사용자 확인 게이트 (모달 vs inline 카드 vs 무게이트) ## 결정 ### 결정 1: in-process Dart tool runtime - 별도 process 띄우지 않는다. `lib/ai/tools/` 하위에 `ToolDefinition` + `ToolDispatcher` + 핸들러를 Dart 로 작성. - flutter_gemma 의 `createChat(tools: [...], toolChoice: ToolChoice.auto)` 가 모델 ↔ Dart 사이의 protocol layer 역할. - `FunctionCallResponse` 를 받으면 `ToolDispatcher.dispatch(name, args, deps)` 로 라우팅 → `chat.addToolResult(...)` 로 회신. ### 결정 2: R 규칙 enforce 는 tool 핸들러 책임 - 모델 prompt 에 "R3 quota 는 build ≤ 3" 식 안내를 넣지 않는다 (학습 신뢰성 불충분). - 모든 mutation 핸들러는 호출 직전 도메인 함수 (`judgeActiveHabitQuota`, `detectAvoidanceKeywords`, `assertXorProtocol`, `validateTrackerValue` 등) 를 직접 호출. - 위반 시 `ToolErr(code: 'r3_quota' | 'r7_avoidance' | ..., reason: 한국어)` 반환 → 모델이 사용자에게 안내. ### 결정 3: tool schema source-of-truth = Dart 코드 - 각 `ToolDefinition.parametersSchema` 는 Dart `Map` 리터럴 (draft-07 JSON Schema 형태). - yaml/json 별도 파일 두지 않는다. - 이유: 1. 핸들러 시그니처와 schema 가 같은 파일에 있어 drift 방지 2. yaml 추가 시 codegen + 버전 동기화 부담 3. IDE 자동완성 / rename / find-usages 활용 ### 결정 4: destructive tool = 모달 Confirm 게이트 의무 - `add_habit`, `log_tracker_entry(value=done)` 등 mutation tool 은 `isDestructive=true` 플래그. - Dispatcher 가 호출 전 `ConfirmGate.show(context, tool, args)` 로 AlertDialog 표시 → 사용자 OK 시에만 핸들러 실행. - inline 카드 (chat 메시지 안에) 대신 모달 채택 — 시각적 안전성과 모달 API 단순성을 위해. - 사용자 결정 (2026-06-15). ## 결과 - 새 디렉토리 `lib/ai/tools/` 와 `chat_screen.dart`. - `LlmService` 인터페이스에 `sendChatTurn(...)` 추가 — `MockLlmService` 도 갱신. - 별도 server / process 추가 없음. 의존성 증가 없음. - 향후 외부 서비스 (예: 클라우드 카탈로그 sync) 도입 시 핸들러 내부에서 fetch 하는 것으로 충분 — MCP 도입 부담 없음. ## 영향 / 후속 - (+) tool latency in-process Dart 호출이라 < 100ms. MCP IPC 오버헤드 없음. - (+) R 규칙 단일 SoT — Repository 가 검증하므로 UI/CLI/Chat 모두 동일 동작. - (-) MCP 표준 호환 X — 외부 tool 가 MCP 서버로 연동하려면 별도 어댑터 필요. Phase 1 범위 아님. - (-) Dart schema 가 수십 개 넘으면 가독성 부담 — ≥ 20 tool 시 ADR 후속 (yaml/codegen 도입 재검토). ## 대안 (기각) - **A. MCP 서버 별도 띄우기**: 모바일에서 native process 띄우려면 platform channel + lifecycle 관리. 메모리 +N MB. 표준 호환 외 이득 없음 — Phase 1 부적합. - **B. Prompt-only 안내**: 모델이 R 규칙을 학습 못 한다는 게 이미 검증됨 (#218 OQ-1 시점). 안전한 mutation 불가. - **C. inline 확인 카드**: chat 메시지 흐름에 자연스럽지만 사용자가 다른 메시지에 묻혀 무심코 진행 위험. 모달이 더 안전. - **D. yaml schema**: codegen 부담. Dart 단일 SoT 가 단순. ## 참고 - 설계서: `docs/design/260-gemma-tool-calling/README.md` - 관련 ADR: ADR-0003 (on-device Gemma 채택) - 관련 Redmine: #260