AI가 쓸 수 있는 도구를 직접 만들었다 — MCP 서버 제작기

2026년 08월 28일 · AI 자동화, 개발 일지

사람이 쓰는 화면과 AI가 쓰는 도구가 같은 코어를 거쳐 각 플랫폼으로 이어지는 구조

앞 글에서 발행 도구를 만들었다고 했는데, 그 얘기를 자세히 풀어보려 한다.

시작은 단순한 불편이었다. 글을 다 쓰고 나면 그때부터 손이 간다. 관리자 화면에 로그인하고, 새 글을 열고, 본문을 붙여넣고, 카테고리를 고르고, 태그를 달고, 이미지를 올려 대표 이미지로 지정하고, 예약 시각을 맞춘다. 글 하나에 몇 분씩 걸리는데 판단이 필요한 구간은 거의 없다. 전부 같은 순서의 반복이다.

이걸 대화로 시키고 싶었다. “이 글 올려줘” 한 줄이면 끝나게.

대화로 시키려면 도구가 있어야 한다

AI에게 글을 올려달라고 하려면 AI가 실제로 실행할 수 있는 무언가가 필요하다. 그냥 “올려줘”라고 하면 AI는 할 수 있는 게 없다.

여기서 쓴 게 MCP(Model Context Protocol)다. 이름은 거창한데 하는 일은 단순하다. 내가 만든 함수를 AI가 부를 수 있게 등록해두는 규격이다. “이런 이름의 도구가 있고, 이런 인자를 받고, 이런 걸 한다”고 선언해두면 AI가 필요할 때 알아서 호출한다.

그래서 발행 로직을 함수로 만들고 MCP 서버로 감쌌다. 이제 “이 글 올려줘”라고 하면 AI가 발행 함수를 찾아 호출한다.

사람과 AI가 같은 걸 보게 만들었다

여기서 첫 번째 설계 문제가 나왔다.

사이트를 등록하는 건 사람이 하는 일이다. 주소를 넣고 비밀번호를 넣는 걸 대화로 하고 싶진 않았다. 반면 글을 올리는 건 AI가 할 일이다. 그러면 사람이 쓰는 화면과 AI가 쓰는 도구, 둘을 만들어야 한다.

문제는 이 둘이 따로 놀면 안 된다는 것이다. 사람이 화면에서 사이트를 추가했는데 AI는 그걸 모르면 아무 의미가 없다.

그래서 양쪽이 같은 코어를 보게 했다. 사이트 목록과 설정은 한 곳에만 있고, 사람용 화면도 AI용 도구도 거기를 읽고 쓴다. 구조로 그리면 이렇다.

사람 ─▶ 화면(GUI) ┐
                  ├─▶ 공유 코어 (사이트 레지스트리) ─▶ 각 플랫폼
AI  ─▶ MCP 도구  ┘

덕분에 사람이 화면에서 사이트를 하나 추가하면 AI가 바로 그 사이트에 글을 올릴 수 있다. 동기화 같은 걸 따로 만들 필요가 없었다. 애초에 하나니까.

자격증명은 암호화해서 저장했다. 사이트 비밀번호를 평문으로 두는 건 아무래도 찜찜해서 키를 따로 두고 암호화한 뒤 넣었다.

API가 있는 곳과 없는 곳

두 번째 문제가 더 까다로웠다.

워드프레스는 REST API가 있다. 주소로 요청을 보내면 글이 올라간다. 그런데 다른 한 곳은 공식 API가 없었다. 브라우저를 직접 띄워서 사람이 하는 것처럼 로그인하고 글을 붙여넣어야 했다.

성격이 완전히 다르다. 한쪽은 HTTP 요청 몇 번이고 다른 쪽은 브라우저를 켜서 화면을 조작하는 일이다. 걸리는 시간도 다르고, 실패하는 방식도 다르다.

이걸 그대로 두면 부르는 쪽이 매번 분기해야 한다. “이 사이트는 워드프레스니까 이렇게, 저 사이트는 저기니까 저렇게.” 사이트가 늘어날 때마다 그 분기가 여기저기 퍼진다.

인터페이스를 먼저 정하고 채웠다

그래서 발행이라는 행위의 모양부터 정했다. 어디에 올리든 결국 하는 일은 “사이트와 글을 받아서 발행하고 결과를 돌려주는 것”이다.

class Publisher(Protocol):
    name: str

    async def publish(self, site: Site, post: Post) -> PublishResult:
        """글을 발행하고 결과를 반환. 예외 대신 PublishResult.error 로 담는다."""
        ...

그리고 사이트마다 어느 구현을 쓸지 골라주는 함수를 하나 뒀다.

def get_publisher(site: Site) -> Publisher:
    pub = _INSTANCES.get(site.platform)
    if pub is None:
        raise ValueError(f"지원하지 않는 플랫폼입니다: {site.platform}")
    return pub

이러니까 부르는 쪽 코드가 단순해졌다. 사이트를 받아서 퍼블리셔를 얻고 publish를 부르면 끝이다. 그게 REST를 때리는지 브라우저를 띄우는지 알 필요가 없다.

여기서 하나 신경 쓴 게 있다. 예외를 던지지 않고 결과 안에 에러를 담기로 한 것이다. 브라우저 자동화는 실패하는 방식이 워낙 다양해서, 예외로 처리하면 부르는 쪽이 무슨 예외가 올지 몰라 전부 감싸야 한다. 결과 객체에 성공 여부와 에러 메시지를 같이 담으니 처리가 한 갈래로 정리됐다.

전용 도구와 비상구

세 번째는 도구를 몇 개나 만들 것인가였다.

처음엔 발행 도구 하나면 될 줄 알았다. 그런데 쓰다 보니 계속 늘어났다. 사이트 목록을 봐야 하고, 카테고리 이름을 알아야 하고, 이미 올린 글을 확인해야 하고, 올린 글을 고쳐야 하고, 이미지를 만들어야 했다. 지금은 스무 개쯤 된다.

그렇다고 모든 경우를 전용 도구로 만들 수는 없다. 예상 못 한 작업이 나올 때마다 서버를 고쳐야 하니까.

그래서 비상구를 하나 뒀다. 워드프레스 REST를 그대로 호출하는 패스스루 도구다. 전용 도구가 없는 작업은 이걸로 처리한다. 대신 호출 기록을 남겨서, 자주 쓰이는 게 보이면 전용 도구로 승격시킨다.

이 구조가 꽤 잘 맞았다. 흔한 작업은 전용 도구로 간결하게, 드문 작업은 비상구로 유연하게 처리하면서, 무엇이 흔한지는 실제 사용 기록이 알려준다.

그리고 문제가 하나 터졌다

여기까지 만들고 한동안 잘 썼다. 그런데 어느 날 글 다섯 편을 올리고 사후 교정을 하는 작업을 한 세션에서 처리했다가, 토큰이 예상보다 훨씬 많이 나갔다.

원인을 뜯어보니 도구 응답이었다. 이건 다음 글에서 따로 다루려 한다. MCP 서버를 만드는 튜토리얼은 많은데 이 문제를 다루는 글은 거의 못 봤고, 실제로 겪어보니 설계에서 제일 중요한 부분이었다.

정리하면

MCP 서버를 만들면서 배운 건 세 가지다.

하나는 사람이 쓰는 것과 AI가 쓰는 것이 같은 데이터를 봐야 한다는 것이다. 따로 만들면 반드시 어긋난다.

다른 하나는 서로 다른 대상을 묶을 때 인터페이스를 먼저 정한다는 것이다. REST와 브라우저 자동화처럼 성격이 딴판인 것도, 하는 일의 모양이 같으면 같은 이름으로 부를 수 있다.

마지막은 전용 도구만으로도 패스스루만으로도 부족하다는 것이다. 둘을 같이 두고, 무엇을 승격시킬지는 사용 기록으로 판단하는 게 나았다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다