ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AI 에이전트가 보는 surface를 8개로 좁히다 — deep-wiki MCP gateway
    IT 2026. 5. 28. 22:00
    AI 에이전트가 보는 surface를 8개로 좁히다 — deep-wiki MCP gateway

    AI 에이전트(Claude Code, Claude Desktop 같은 도구)와 함께 일하다 보면 한 가지 한계가 자주 보인다. "에이전트가 내 코드 베이스를 정말 아는 게 아니라, 매 세션마다 처음부터 본다." 새 세션을 시작할 때마다 같은 repo의 구조를 다시 설명해야 하고, repo 사이의 의존 관계를 다시 그려야 한다. RAG(Retrieval-Augmented Generation, 외부 지식 베이스에서 관련 문서를 검색해 prompt에 끼우는 패턴)는 부분 해법이지만, 일반적인 검색이라 우리 도메인 구조를 깊이 알지는 못한다.

    그래서 deep-wiki를 만들면서 한 가지를 같이 잠갔다. "AI 에이전트가 우리 시스템에 필요할 때 호출할 수 있는 도구 8개를 MCP gateway로 둔다." 에이전트가 한 번 묻고, MCP gateway가 결정적 데이터(graph DB·Qdrant)와 미리 생성해 둔 위키 본문을 한 패키지로 합성해 돌려준다.

    이 글은 그 MCP gateway가 어떻게 구성되는지, 그리고 왜 이 한 겹이 1인 환경에서도 가치를 가지는지의 기록이다. 요지는 단순하다 — 에이전트가 직접 알아야 할 대상(백엔드 4곳)을 서버 한 겹 뒤로 숨기고, 에이전트에게는 도구 8개만 보이게 한다. 에이전트는 어디서 어떻게 데이터를 모으는지 알 필요 없이, 잘 정의된 도구만 부르면 된다.

    1. 배경 — 왜 MCP gateway가 한 layer 더 필요한가

    MCP(Model Context Protocol, 2024년 말 Anthropic이 공개한 개방 표준)는 AI 에이전트가 외부 도구·데이터 소스를 호출하는 표준 인터페이스다. "AI 모델과 외부 시스템을 잇는 USB-C 포트"에 비유되곤 한다 — 일관된 JSON-RPC 규약으로 도구를 노출하면 모든 MCP 호환 클라이언트(Claude Desktop·Cursor 등)가 같은 방식으로 호출할 수 있다. 일반적인 사용법은 "내 GitHub repo를 보여줘", "이 DB를 조회해줘" 같은 직접 호출이다. 그런데 우리 시스템에서는 한 layer 더 — MCP gateway가 필요했다. 이유가 명확하다.

    diagram

    핵심은 "에이전트가 알아야 할 surface를 줄인다"는 것이다. AI 에이전트가 graph DB·Qdrant·gemma·wiki 파일을 각각 알면 surface가 너무 넓어지고, 새 백엔드 추가 시 에이전트 prompt도 같이 갱신해야 한다. MCP gateway가 그 surface를 8개 도구로 압축하면, 에이전트는 8개 도구 surface만 알면 충분하고 백엔드 변경은 MCP gateway 안에서 흡수된다.

    한 가지 더 중요한 게 있다. "deep-wiki의 graph_extractor 변경이 어떤 repo에 영향을 미치나?"는 graph DB(cross-project edges) + wiki-output(영향 받는 모듈 페이지) + 설계 결정 기록을 모두 합쳐야 답할 수 있다. MCP gateway의 context_pack 도구가 이런 다중 출처 합성을 한 호출로 해 준다. AI 에이전트는 한 번 묻고, MCP gateway가 백엔드 4곳에서 정보를 모아 한 패키지로 반환한다.

    2. 8개 MCP 도구 — 책임 분담

    diagram

    8개 도구는 세 그룹으로 분배된다. wiki_*는 "이게 뭐냐"(콘텐츠), graph_*는 "어떻게 연결돼 있냐"(구조), rag_search + context_pack은 "이걸 다 합치면 뭐냐"(의미·합성). 세 그룹의 책임이 깔끔하게 분리돼 있어서 AI 에이전트가 어떤 도구를 부를지 결정하기 쉽다.

    가장 강력한 도구는 context_pack이다. AI 에이전트가 "의도(intent) + target"을 주면, MCP gateway가 wiki·graph·RAG·설계 결정 기록을 모두 합쳐 50K 토큰 단일 패키지로 반환한다. 에이전트는 그 패키지를 한 번 받으면 그 작업에 필요한 거의 모든 맥락을 갖게 된다. 7번의 매번 다른 도구 호출이 1번의 합성 호출로 압축된다.

    3. 코드로 보면 — 도구 하나를 끝까지 채우면

    8개 도구를 다 늘어놓는 대신, 가장 단순한 축의 도구 하나(wiki_get_module)와 모든 응답에 붙는 _meta만 끝까지 채워 보자. 나머지 도구도 골격은 같다.

    # mcp_gateway/server.py — _meta와 도구 하나(wiki_get_module)
    
    from pathlib import Path
    from mcp.server.fastmcp import FastMCP
    
    ROOT = Path.home() / "projects" / "deep-wiki"
    WIKI_OUTPUT = ROOT / "wiki-output" / "private"
    SUMMARY_MAX_CHARS = 1200
    
    mcp = FastMCP("deep-wiki")
    
    
    def _meta(verified=False, tier="none", *,
              tokens_estimate=0, truncated=False, evidence=None) -> dict:
        """모든 도구 응답에 붙는 출처·검증 메타."""
        return {
            "claude_verified": verified,
            "verification_tier": tier,           # none < haiku < sonnet (검증에 쓴 모델)
            "tokens_estimate": tokens_estimate,  # 돌려준 내용의 대략 크기
            "truncated": truncated,              # summary 등으로 잘렸으면 True
            "evidence": evidence or [],          # 출처: "<source>@<commit_sha>"
        }
    
    
    def _estimate_tokens(text: str) -> int:
        """토크나이저 없이 대략 추정 (~4자/토큰)."""
        return len(text) // 4
    
    
    def _split_frontmatter(text: str) -> tuple[dict, str]:
        """맨 앞 YAML frontmatter를 평면 dict + 본문으로 분리."""
        if not text.startswith("---"):
            return {}, text
        close = text.find("\n---", 3)
        if close == -1:
            return {}, text
        fm = {}
        for line in text[3:close].splitlines():
            key, sep, val = line.partition(":")
            if sep:
                fm[key.strip()] = val.strip()
        return fm, text[close + 4:].lstrip("\n")
    
    
    @mcp.tool()
    def wiki_get_module(module_id: str, depth: str = "summary") -> dict:
        """wiki-output/private/<module_id>/index.md 를 읽어 돌려준다.
        summary = 첫 '## ' 직전(리드 섹션), full = 본문 전체."""
        module_dir = WIKI_OUTPUT / module_id
        page = module_dir / "index.md"
        if not page.is_file():                       # 없는 모듈 → 빈 응답 (graceful)
            return {"module_id": module_id, "depth": depth,
                    "content": "", "adr_refs": [], "meta": _meta()}
    
        front, body = _split_frontmatter(page.read_text(encoding="utf-8"))
    
        if depth == "summary":
            cut = body.find("\n## ")
            content = (body[:cut] if cut != -1 else body)[:SUMMARY_MAX_CHARS].rstrip()
        else:
            content = body
        truncated = len(content) < len(body)         # 잘렸으면 True
    
        adr_refs = sorted(p.name for p in module_dir.glob("[0-9][0-9][0-9]-*.md"))
        evidence = ([f"{front['source']}@{front['commit_sha']}"]
                    if "source" in front else [])
    
        return {
            "module_id": module_id, "depth": depth,
            "content": content, "adr_refs": adr_refs,
            "meta": _meta(tokens_estimate=_estimate_tokens(content),
                          truncated=truncated, evidence=evidence),
        }
    

    이 도구가 하는 일은 파일 하나를 읽어 주는 것뿐이다. 그런데 내용만 던지지 않고 세 가지 단서를 같이 채운다는 점이 포인트다.

    • evidence — 이 내용이 어디서 왔는지. 페이지 frontmatter의 원본 경로와 commit SHA를 묶어 "docs/index.md@1db4c97"처럼 박는다. 에이전트가 출처를 추적하거나 grounding을 확인할 수 있다.
    • truncatedsummary로 부르면 첫 ## 섹션 직전(리드 단락)까지만 잘라 주는데, 잘렸으면 True다. 에이전트는 이걸 보고 "개요로 충분한지, full로 다시 부를지"를 판단한다.
    • tokens_estimate — 돌려준 내용의 대략적 크기. 에이전트가 context 예산을 가늠하는 데 쓴다.

    즉 같은 파일을 읽어도 '내용'만이 아니라 '어디서 왔고, 잘렸는지, 얼마나 큰지'를 함께 준다. 그리고 이 메타에는 한 칸이 더 있다 — 가장 중요한 verification_tier다.

    여기서 한 가지 배경이 필요하다. deep-wiki의 위키 본문은 사람이 손으로 쓴 게 아니라 LLM이 코드를 읽고 생성한다. 그래서 "이 설명이 실제 코드와 맞는지"를 다시 확인하는 검증 단계를 거치는데, 모든 문장을 같은 강도로 검증하지는 않는다. 가벼운 내용은 작은 모델(haiku)로 빠르게 1차 확인하고, 중요한 내용은 더 큰 모델(sonnet)로 한 번 더 본다. 그래서 "얼마나 검증됐나"는 none → haiku → sonnet 순으로 올라가는 등급이 된다.

    문제는, 에이전트에게 검색 결과만 툭 던져 주면 그게 얼마나 믿을 만한지 에이전트가 알 수 없다는 것이다. 꼼꼼히 검증된 문장이든 빠르게 훑고 지나간 문장이든, 에이전트는 똑같이 "사실"로 받아들이고 그대로 행동해 버린다. LLM이 만든 글이라 틀릴 수 있는데도 말이다. 그래서 모든 도구 응답에 _meta()"이 정보가 어느 단계까지 검증됐는지"를 등급(verification_tier)으로 함께 붙여 보낸다.

    이러면 에이전트의 행동이 달라진다. 등급이 낮은 정보는 "한 번 더 확인하고 쓰자", 높은 정보는 "믿고 바로 진행"처럼 같은 데이터라도 다르게 다룰 수 있다. 내용뿐 아니라 "얼마나 믿을 수 있는지"까지 함께 받는 것 — 이 작은 메타 한 줄이 에이전트의 다음 판단을 바꾼다.

    4. transport — stdio와 HTTP+SSE 두 갈래

    도구를 부르는 쪽(클라이언트)이 한 종류가 아니다. 같은 컴퓨터 안에서 도는 Claude Code도 있고, 다른 기기나 여러 클라이언트가 네트워크 너머에서 붙는 경우도 있다. 이렇게 "어떻게 연결하느냐"를 transport라고 부른다. MCP gateway는 두 가지를 지원한다 — 같은 컴퓨터면 stdio(프로그램을 직접 실행해 표준 입출력으로 대화하는 방식), 네트워크 너머면 HTTP+SSE(:8190 주소로 접속하는 방식).

    # mcp_gateway/server.py:main() — 도구는 그대로, transport만 분기
    
    def main() -> int:
        parser = argparse.ArgumentParser()
        parser.add_argument("mode", choices=["stdio", "serve", "manifest"])
        parser.add_argument("--port", type=int, default=8190)
        args = parser.parse_args()
    
        if args.mode == "manifest":
            # .well-known/mcp/tools 매니페스트만 써내고 종료
            out = write_tools_manifest()
            ...
            return 0
    
        if args.mode == "stdio":
            mcp.run("stdio")          # Claude Code 로컬: 프로세스 직결
            return 0
    
        # serve mode — HTTP+SSE MCP transport
        mcp.settings.host = "127.0.0.1"
        mcp.settings.port = args.port
        mcp.run("sse")                # 원격·다중 클라이언트
        return 0
    

    코드를 보면 규율 하나가 드러난다. stdioHTTP+SSE든 바뀌는 건 마지막 mcp.run(...) 한 줄뿐이고, 도구 8개는 위쪽 @mcp.tool()한 번만 정의돼 있다. transport는 "문으로 들어오느냐 창문으로 들어오느냐"의 차이일 뿐, 일단 안에 들어오면 보이는 도구와 동작은 똑같다. 그래서 연결 방식이 하나 더 늘어도 도구를 두 벌로 관리할 일이 없다 — 에이전트가 믿고 부를 도구 목록과 검증 등급은 server.py 한 곳에서만 산다.

    5. 운영 결과 — Claude Code 세션의 cold start 시간 단축

    이 MCP gateway를 도입한 후 가장 즉시 체감된 효과는 Claude Code 세션의 cold start가 짧아진 것이었다. 새 세션을 시작할 때마다 wiki_list_modules 한 번 + context_pack(intent="overview") 한 번이면 우리 22 repo의 전체 구조와 cross-module 관계를 단번에 받는다. 이전엔 tree로 디렉토리를 보고 README를 여러 개 읽고 cross-module 관계를 추측했던 부분이 자동화됐다.

    마무리 — Agent surface를 좁히는 결정이 가져오는 자유

    AI 에이전트와 함께 일하는 시스템을 설계할 때 자주 잊는 한 가지가 있다. "에이전트가 알아야 할 surface를 좁힐수록, 백엔드 자유도가 넓어진다." 에이전트가 8 도구만 알면, 백엔드를 SQLite에서 Postgres로 바꿔도, gemma에서 다른 LLM으로 갈아 끼워도, 에이전트는 변경을 느끼지 않는다. 한 단어로 독립성이다.

    결국 핵심은 화려한 아키텍처가 아니다. "각 단계가 자기 일만 알면 충분한 경계"다. 에이전트는 8 도구만, MCP gateway는 4 백엔드로의 라우팅·합성만, 각 백엔드는 자기 데이터만 안다. 여기서 MCP gateway는 스스로 판단하는 또 하나의 에이전트가 아니라 — 정해진 요청을 정해진 백엔드로 넘기는 서버 한 겹일 뿐이다. 그 단순함이 핵심이다. 경계가 분명해서 한 컴포넌트를 갈아 끼워도 나머지가 멈추지 않고, 좁힌 surface가 곧 바꿀 수 있는 자유가 된다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.