MCP
프롬프트로 못 미치는 자리에 도구를 붙인다.
프롬프트가 못 미치는 곳
프롬프트로는 지식도 성격도 넣을 수 있다. 하지만 "지금 세 명이 일산·분당·강동에서 출발하는데 어디서 만나야 공평한가", "이 마인크래프트 아이템 정확한 레시피가 뭔가" 같은 질문은 프롬프트로 못 푼다. 답이 모델의 기억 밖, 실시간이고 정확해야 하는 외부 세계에 있기 때문이다. RAG가 여기에 근거를 붙였다면, MCP는 도구를 붙인다.
MCP(Model Context Protocol)를 한 줄로 하면, LLM 호스트에 외부 능력을 표준 규격으로 꽂는 포트다. USB처럼, 한 번 꽂으면 그다음부턴 대화로 시키기만 하면 호스트가 알아서 도구를 부른다. 중요한 건 방향이다. MCP는 봇을 조종하는 게 아니라, LLM에게 도구를 쥐여주는 쪽이다. 이 구분이 나중에 noon-desktop에서 "왜 여기선 MCP가 안 맞는가"를 갈랐다.
나는 이걸 세 각도에서 겪었다. 서버를 직접 만들어 남의 호스트에 올려보고(그어봄), 그 서버를 다시 내 앱이 호스트로 꽂아 써보고, 외부 MCP를 개발 도구로 편입하고, 마지막으로 MCP가 안 맞는 자리에선 다른 방식을 골랐다.
MCP는 경계다
MCP의 실체는 경계(seam) 하나다. 한쪽엔 호스트(LLM 앱), 다른 쪽엔 서버(능력)가 있다. 호스트는 tools/list로 서버가 뭘 할 수 있는지 발견하고, tools/call로 실행을 넘긴다. 서버가 노출하는 도구는 이름·설명·입력 스키마, 그리고 이 도구가 읽기만 하는지·파괴적인지 같은 어노테이션으로 자기를 소개한다. 호스트는 그 소개만 보고 도구를 부른다. 서버 안에서 무슨 일이 벌어지는지는 몰라도 된다.
이 경계가 왜 좋은지는 만들어봐야 안다. 내부 함수를 MCP로 감싸기만 하면 절차만 배우고 "왜 MCP"는 못 배운다. 그래서 경계의 양쪽에 다 몸을 댔다.
자작 MCP, 그어봄
그어봄은 내가 처음부터 만든 MCP 서버다. 단톡방에서 "그래서 우리 어디서 봄?"에 답한다. 여러 출발지를 받아 모두에게 가장 공정한 중간 지점을 계산한다. 지도 앱은 "A→B 한 사람 경로"만 보지만, 그어봄은 여러 명을 동시에 놓고 "가장 멀리서 오는 사람도 덜 고생하는" 한 점을 찾는다.
서버 자체는 얇다. FastMCP 위에 도구를 단 하나 올렸다(find_meeting_point). 전송은 stateless Streamable HTTP다. 도구엔 PlayMCP 심사가 요구하는 어노테이션 다섯 개를 전부 달았다. 읽기 전용이고, 파괴적 동작이 없고, 같은 입력이면 같은 결과이고, 외부 API와 상호작용한다는 것을 도구가 스스로 선언한다. 도구 설명(description)은 국·영문을 병기했다. 호스트의 LLM이 이걸 읽고 언제 부를지, 결과를 어떻게 보여줄지 정하기 때문이다.
여기서 이 프로젝트를 규정하는 판단이 하나 있다. 도구는 데이터만 반환하고, 사람에게 보여줄 문장은 호출하는 LLM이 만든다. find_meeting_point는 후보 지역·각자 소요시간·선정 이유를 구조화된 dict로 돌려줄 뿐, 완성된 문장을 만들지 않는다. 도구 안에 또 LLM을 넣지 않는다. 판단은 이미 호스트의 LLM이 하니까, 서버는 순수한 계산과 데이터에만 집중한다. 경계를 깨끗하게 지키는 것이다.
경계는 서버 밖에도 있다. 자차 경로는 카카오내비, 대중교통은 ODsay로 채점하는데, 카카오 대중교통 API가 아직 안 열려 ODsay를 임시로 쓴다. 그래서 대중교통 제공자를 파일 하나(odsay.py)로 격리해, API가 열리면 그 한 파일만 갈아끼우면 되게 했다. 안이든 밖이든, 바뀔 것과 안 바뀔 것 사이에 선을 긋는 게 같은 사고다.
이 서버는 카카오 PlayMCP(AGENTIC PLAYER 공모전)에 올려, 남의 호스트가 실제로 내 도구를 발견하고 호출하는 걸 확인했다. 경계의 서버 쪽을 겪은 것이다.
경계의 반대쪽, 내가 만든 서버를 내 앱이 쓴다
같은 서버를 이번엔 호스트 쪽에서 다시 만났다. 새김AI 백엔드를 MCP 호스트/클라이언트로 만들어, 배포된 그어봄 엔드포인트에 붙였다(backend/app/services/mcp_client.py). 공식 mcp SDK의 Streamable HTTP 클라이언트로 연결해, tools/list로 도구를 받아와 에이전트의 도구 스키마에 머지하고, 에이전트가 그 도구를 부르면 tools/call로 라우팅한다.
머지할 때 이름 앞에 mcp__<서버>__<도구> 네임스페이스를 붙인다. 앱이 이미 가진 로컬 도구들과 충돌하지 않게 하려는 것이다. 도구 목록은 5분 캐시하고, 서버가 죽어 있으면 조용히 건너뛴다(앱 전체가 멈추지 않게). 서버 하나가 실패해도 나머지는 산다.
내가 만든 MCP 서버를, 내가 만든 다른 앱이 호스트로 붙여 에이전트가 쓴다. 경계의 양쪽을 한 손으로 잡아보는 것 — 이게 "왜 MCP"를 배우는 유일한 길이었다. 서버만 만들면 예식(ceremony)만 배우고, 호스트만 쓰면 남의 도구를 소비하는 데 그친다. 둘을 다 하면 그제야 이 경계가 무엇을 사고 무엇을 파는지 보인다.
도구를 편입한다, 외부 MCP
방향을 뒤집으면, 남이 만든 MCP를 내 개발 환경이 소비하는 쪽도 있다. Claude Code를 호스트로 써서, 필요한 능력을 MCP로 꽂아 에이전트의 손에 쥐여준다.
원칙은 하나다. 많이 꽂는 게 좋은 게 아니다. 꽂는 만큼 도구 목록이 무거워지고 컨텍스트를 먹는다. 그래서 실제로 쓸 것만 꽂는다(GitHub는 gh CLI로 이미 커버돼 MCP로는 안 꽂았다). 이 편입 얘기는 AI 하네스 문서에서 스튜디오 전체 운영의 맥락으로 다시 다룬다.
MCP가 안 맞는 자리, noon-desktop 전문가 모드
noon-desktop의 "전문가 모드"는 같은 그라운딩 철학을, MCP 없이 구현한 사례다. 여기서 MCP를 안 쓴 게 핵심이다.
전문가 모드의 원칙은 그어봄·외부 MCP와 똑같다. 전문가 모드 = 프롬프트 한 줄이 아니라 **프롬프트(도메인 지식) + 전용 도구(그라운딩 소스)**다. 마인크래프트 모드를 켜면, noon은 레시피·몹·인챈트 질문에 기억으로 답하지 않고 search_minecraft_wiki 도구로 공식 위키(minecraft.wiki)를 실제로 읽고 답한다. 프롬프트-온리는 정확한 수치·버전 변경에서 환각하기 때문이다. 도구는 켜진 모드에 따라 게이팅된다(마크 모드일 때만 위키·레시피 도구가 도구 목록에 붙는다).
그런데 이 도구들은 MCP가 아니라 모델의 네이티브 tool-use(함수 콜링)로 붙였다. noon은 Anthropic Messages API에 도구 정의를 직접 실어 보내고, 응답이 tool_use면 도구를 실행해 결과를 되먹이는 루프를 스스로 돈다. search_minecraft_wiki는 MediaWiki API를 한 번 호출해 검색과 본문 추출을 함께 끝낸다.
왜 MCP를 안 썼나. 마인크래프트 MCP 서버들을 조사해보니 전부 봇 조종(mineflayer)·서버 관리(RCON)·모딩 문서용이었다. 게임을 대신 플레이하거나 서버를 운영하는 도구지, "플레이어에게 지식을 알려주는 컴패니언"엔 맞지 않았다. MCP의 방향은 능력을 꽂는 것이지만, 꽂을 만한 능력이 이 도메인엔 없었다. 그래서 위키 API를 직접 두르고, 도구는 모델의 네이티브 tool-use로 붙였다. 철학(프롬프트 + 전용 도구 그라운딩)은 같고, 기계장치만 다르다.
이게 MCP를 겪으며 얻은 가장 정직한 경계선이다. MCP는 도구 그라운딩의 한 가지 표준 규격이지 유일한 길이 아니다. 쓸 만한 서버가 이미 있고 경계를 공유할 이유가 있으면 MCP가 낫고, 도메인 API 하나만 두르면 되는 좁은 자리엔 네이티브 tool-use가 가볍고 정확하다.
Decision Log
그어봄의 find_meeting_point는 후보·시간·이유를 구조화 dict로만 돌려준다. 사람에게 보여줄 문장은 호스트의 LLM이 만든다. 도구 안에 또 LLM을 넣지 않는다. 판단은 이미 호스트가 하니, 서버는 순수 계산과 데이터에만 집중해 경계를 깨끗하게 지킨다.
서버(그어봄)를 만들어 남의 호스트(PlayMCP)에 올리고, 그 서버를 다시 내 앱(새김AI)이 호스트로 붙여 소비했다. 내부 함수를 MCP로 감싸기만 하면 예식만 배운다. 서버와 호스트 양쪽을 한 손으로 잡아야 "왜 MCP"가 보인다.
그어봄의 대중교통 제공자(ODsay)는 카카오 API가 열리면 교체될 임시 조각이라, odsay.py 한 파일로 격리했다. 서버 안이든 MCP 경계든, 바뀔 것과 안 바뀔 것 사이에 선을 긋는 게 같은 사고다.
noon-desktop 마크 전문가 모드는 그라운딩이 필요했지만, 쓸 만한 마크 MCP가 전부 봇 조종·서버 관리용이었다. 그래서 위키 API를 직접 두르고 모델의 네이티브 tool-use로 도구를 붙였다. MCP는 도구 그라운딩의 한 규격이지 유일한 길이 아니다.
MCP는 꽂는 만큼 도구 목록이 무거워지고 컨텍스트를 먹는다. context7·playwright처럼 실제로 쓰는 것만 편입하고, gh CLI로 이미 커버되는 GitHub는 MCP로 중복해 꽂지 않았다.
배운 것
MCP는 "서버를 하나 만드는 일"이 아니라 경계를 다루는 감각이다. 호스트와 서버 사이에 도구라는 계약을 두고, 서버는 데이터만 내놓고, 호스트의 LLM이 판단한다. 이 선을 깨끗이 그으면 서버는 얇아지고, 도구는 어디에 꽂혀도 똑같이 동작한다.
그리고 MCP는 목적이 아니라 수단이다. 도구 그라운딩이라는 목표가 먼저 있고, MCP는 그걸 이루는 여러 방법 중 표준 규격 하나다. 그어봄처럼 남과 경계를 공유할 서버는 MCP로 만들고, noon-desktop처럼 좁은 도메인 API 하나면 되는 자리는 네이티브 tool-use로 가볍게 간다. 세 각도를 다 겪고 나서야, "MCP를 쓴다"가 아니라 "이 자리엔 MCP, 저 자리엔 tool-use"라고 말할 수 있게 됐다.