KAKAO MAP을 이용한 MCP 서버 만들기!

데일리·2025년 6월 12일
1

TIL

목록 보기
14/16


정말 기술 트렌드 빠르게 바뀌는만큼 요즘 아주 핫한 기술인 MCP라는 기술이 등장했습니다!
해당 기술을 사용하면 이제 개발도 다 LLM이 해주고 다른 서비스와 연결도 LLM이 해주다는데 그럼 난 이제 뭘해먹고 살아야...🍗
그렇다면 MCP가 무엇인지 또 어떻게 적용할 수 있는지 알아보도록 하겠습니다~!

📌 MCP란?

Model Context Protocol의 준말으로 AI 모델이 다양한 데이터 소스와 연결되어 실시간으로 정보를 주고받을 수 있도록 설계된 프로토콜입니다.

말이 좀 어려울 수 있는데 쉽게 말하면 우리가 사랑하는 Chat GPT는 주로 정보 검색용으로 사용하는데요, 지금까지는 이런 행동이 불가능했습니다.

"내가 작성한 코드를 GitHub에 Repo를 만들어주고 push 해줘"
"내 PC 디렉토리안에 있는 사진들을 확인해보고 동물 사진들을 골라줘"

위와 같은 외부 라이브러리를 사용하는 서비스들은 GPT 혼자의 힘으로 불가능했는데요, 이런 것들을 가능하게, 즉 AI가 다양한 데이터 소스와 연결되어 더욱 정확하고 답변과 활용 범위가 넓어지는데에 도움을 주는 것이 MCP다 라고 볼 수 있습니다!

만일 좀 더 deep하게 알고 싶으시다면 MCP 영상을 보는 것을 추천드립니다!

현재 이런 MCP 기능을 사용할 수 있게 해주는 LLM이 2가지가 있는데요! Claude, Cursor IDE이 2개가 있습니다. 오늘은 Claude를 활용해서 MCP를 한번 실습해보겠습니다!

📌 MCP 톺아보기

우리가 앞으로 KAKAO API와 연동되는 MCP 서버를 만들건데요 그러기 위해서는 MCP 코드를 한번 뜯어볼 필요가 있습니다. 처음에는 어려워 보일 수 있지만 막상 개발하다보면 별거 아니니 크게 걱정안해도 될 것 같습니다.

우선 해당 GitHub 링크에서 파이썬으로 fastmcp를 구현한 라이브러리를 볼 수 있습니다.

해당 라이브러리의 설명을 살펴보면 아래와 같이 FastMCP 서버를 초기화하고 그 다음에는 필요한 기능을 추가하는 방식으로 되어있습니다.

# server.py
from fastmcp import FastMCP

mcp = FastMCP("MyServer")

@mcp.tool()
def hello(name: str) -> str:
    return f"Hello, {name}!"

if __name__ == "__main__":
    # This is ignored when using `fastmcp run`!
    mcp.run(transport="stdio")

특히 3가지 요소가 있는데

리소스
리소스는 AI가 필요한 읽기 전용 데이터를 제공하는 역할을 합니다. 마치 Rest API의 GET과 비슷하다고 이해하시면 편하실 것 같습니다.

# Static resource
@mcp.resource("config://version")
def get_version(): 
    return "2.0.1"

# Dynamic resource template
@mcp.resource("users://{user_id}/profile")
def get_profile(user_id: int):
    # Fetch profile for user_id...
    return {"name": f"User {user_id}", "status": "active"}

프롬프트
AI 모델이 리소스와 도구를 활용하여 일관되고 유용한 응답을 생성하도록 안내합니다. 쉽게 말하면 LLM의 상호작용을 위한 메시지 템플릿을 정의한다고 보시면 될거 같습니다!

@mcp.prompt
def summarize_request(text: str) -> str:
    """Generate a prompt asking for a summary."""
    return f"Please summarize the following text:\n\n{text}"

도구
가장 중요한 요소로 AI 모델이 외부 시스템에서 작업을 수행할 수 있게합니다. 리소스와는 다르게 도구는 작업을 트리거하고 수행하는 역할을 합니다!

from fastmcp import FastMCP, Context

mcp = FastMCP("My MCP Server")

@mcp.tool
async def process_data(uri: str, ctx: Context):
    # Log a message to the client
    await ctx.info(f"Processing {uri}...")

    # Read a resource from the server
    data = await ctx.read_resource(uri)

    # Ask client LLM to summarize the data
    summary = await ctx.sample(f"Summarize: {data.content[:500]}")

    # Return the summary
    return summary.text

더 자세한 정보는 해당 GitHub 링크 READ.ME에 자세히 나와 있으니 참고하시면 될거 같습니다!

📌 KAKAO MAP MCP 만들어 보기

자 이제 KAKAO MAP API를 이용한 MCP 서버를 만들건데요! 간단하게 이런게 MCP구나라고 체험하고 싶으신 분들은 아래 GitHub링크에서 pull 받으신 다음에 바로 Claude에서 테스트 해보시면 될거 같습니다! 근데 그렇게 어렵지 않아서 한번 만들어보셔도 괜찮을 듯 합니다!!

GitHub 링크

아 그리고 해당 코드는 제가 만들었는데 부족한 부분이 보여도 너그럽게 봐주시면 감사하겠습니다 ㅎㅎ 🙏

그러면 이제 본격적으로 실습을 해볼건데요. 실습하기 전에 필요한게 있습니다. KAKAO API를 사용하기 위해서는 해당 KAKAO DEVELOPERS 에 가입이 필요합니다. 추후에 사용할 REST_API_KEY가 필요하기 때문이죠. 참고로 GitHub에 pull 받고 하시려는 분들도 이 가입은 필수입니다!!

가입이 완료됬으면 우리는 MAP API를 사용해야하기 때문에! 아래의 사진과 같이 설정에서 카카오맵을 무조건 ON! 해주셔야됩니다. 저는 이걸 몰라서 1시간동안 삽질만 했다아는......🤮

저게 됬다면 준비는 다되었습니다. 우선 server를 실행할 수 있는 파일을 만들어야 되는데요!
아래 코드를 보면서 말씀드리겠습니다!

import httpx
from fastmcp import FastMCP

from config import REST_API_KEY

mcp = FastMCP("KAKAO MAP MCP")

API_ENDPOINT = "https://dapi.kakao.com/v2/local"
api_headers = {
    "Authorization": f"KakaoAK {REST_API_KEY}"
}

@mcp.tool(name="search_location", description="search_location")
async def search_location(query:str, x,y,page=1, size=15, sort="accuracy"):
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{API_ENDPOINT}/search/keyword.json",
            params={
                "query": query,
                "x":x,
                "y":y,
                "page": page,
                "size": size,
                "sort": sort,
            },
            headers=api_headers,
        )
        response.raise_for_status()  # Raise an error for bad responses
        return response.text

if __name__ == "__main__":
    # Initialize and run the server
    mcp.run()

우선 위의 코드처럼 mcp.tool을 만들어줍니다. 그러면 저게 하나의 Claude에서 사용할 수 있는 도구가 됩니다. 그래서 해당 도구를 통해 KAKAO MAP API를 통해 우리의 질문에 대한 답변을 하는거죠!

그리고 그 다음에 해야할 것! Claude의 MCP 서버를 사용하려면 /Users/{PC_NAME}/Library/Application Support/Claude/claude_desktop_config.json
해당 json 파일에 MCP 정보를 등록 해주어야 합니다! 참고로 저 Path 정보는 맥북기준입니다!

그래서 저걸 수동으로 등록하기에는 번거로우니 우리는 코드로써 이걸 자동화할 것입니다! 코드는 아래와 같아요~!

import json
import os
import shutil
import sys
from pathlib import Path
from typing import Any, Optional

from fastmcp.cli import claude
from mcp.cli.claude import get_claude_config_path
from mcp.server.fastmcp.utilities.logging import get_logger

logger = get_logger(__name__)


def get_venv_path() -> str:
    """Get the full path to the venv executable."""
    # Check if the Python executable inside the virtual environment is available
    venv_path = Path(os.getenv("VIRTUAL_ENV", "")) / "bin" / "python"
    logger.info(venv_path)
    if not venv_path:
        logger.error(
            "Python executable not found in PATH. Falling back to default Python."
            " Please ensure Python is installed and in your PATH."
        )
        return "python"  # Fall back to default python if not found

    # If the Python executable is found, return its path
    return str(venv_path)


def update_claude_config(
        server_name: str,
        *,
        with_editable: Path | None = None,
        with_packages: list[str] | None = None,
        env_vars: dict[str, str] | None = None,
) -> bool:
    """Add or update a FastMCP server in Claude's configuration.

    Args:
        server_name: Name for the server in Claude's config
        with_editable: Optional directory to install in editable mode
        with_packages: Optional list of additional packages to install
        env_vars: Optional dictionary of environment variables. These are merged with
            any existing variables, with new values taking precedence.

    Raises:
        RuntimeError: If Claude Desktop's config directory is not found, indicating
            Claude Desktop may not be installed or properly set up.
    """
    config_dir = get_claude_config_path()
    venv_path = get_venv_path()

    if not config_dir:
        raise RuntimeError(
            "Claude Desktop config directory not found. Please ensure Claude Desktop"
            " is installed and has been run at least once to initialize its config."
        )

    config_file = config_dir / "claude_desktop_config.json"
    if not config_file.exists():
        try:
            config_file.write_text("{}")
        except Exception as e:
            logger.error(
                "Failed to create Claude config file",
                extra={
                    "error": str(e),
                    "config_file": str(config_file),
                },
            )
            return False

    try:
        config = json.loads(config_file.read_text())
        if "mcpServers" not in config:
            config["mcpServers"] = {}

        # Always preserve existing env vars and merge with new ones
        if (
                server_name in config["mcpServers"]
                and "env" in config["mcpServers"][server_name]
        ):
            existing_env = config["mcpServers"][server_name]["env"]
            if env_vars:
                # New vars take precedence over existing ones
                env_vars = {**existing_env, **env_vars}
            else:
                env_vars = existing_env

        # Dynamically find the path to main.py
        main_py_path = Path(__file__).parent.parent / "main.py"

        if not main_py_path.exists():
            logger.error("main.py not found in the expected directory.")
            return False

        # Build the command and args for the server
        args = [str(main_py_path)]  # Dynamic path to main.py

        if with_editable:
            args.extend(["--with-editable", str(with_editable)])

        # Collect all packages in a set to deduplicate
        packages = {"fastmcp", "mcp-kakao-map"}
        if with_packages:
            packages.update(pkg for pkg in with_packages if pkg)

        # Add all packages with --with
        for pkg in sorted(packages):
            args.extend(["--with", pkg])

        # Convert file path to absolute before adding to command
        server_config: dict[str, Any] = {"command": venv_path, "args": args}

        # Add environment variables if specified
        if env_vars:
            server_config["env"] = env_vars

        config["mcpServers"][server_name] = server_config

        config_file.write_text(json.dumps(config, indent=2))
        logger.info(
            f"Added server '{server_name}' to Claude config",
            extra={"config_file": str(config_file)},
        )
        return True
    except Exception as e:
        logger.error(
            "Failed to update Claude config",
            extra={
                "error": str(e),
                "config_file": str(config_file),
            },
        )
        return False

def install_to_claude_desktop(
    env_vars: list[str] = None,
):
    """
    Install the MCP to Claude Desktop.
    """
    if not claude.get_claude_config_path():
        sys.exit(1)

    from main import mcp

    name = mcp.name
    server = mcp

    with_packages = getattr(server, "dependencies", []) if server else []

    env_dict: Optional[dict[str, str]] = None
    if env_vars:
        env_dict = {}
        for env_var in env_vars:
            key, value = env_var.split("=", 1)
            env_dict[key.strip()] = value.strip()

    if update_claude_config(
        name,
        with_packages=with_packages,
        env_vars=env_dict,
    ):
        ...
    else:
        sys.exit(1)


if __name__ == "__main__":
    from argparse import ArgumentParser

    parser = ArgumentParser(
        description="Install the MCP to Claude Desktop.",
    )
    parser.add_argument(
        "--env",
        "-e",
        action="append",
        help="Environment variables to set for the server.",
    )

    args = parser.parse_args()

    install_to_claude_desktop(
        env_vars=args.env,
    )

코드가 좀 긴데 어려운게 전혀 없습니다. 함수를 하나씩 뜯어보면

  • install_to_claude_desktop: Claude에 MCP를 설치하는 함수
  • update_claude_config: MCP를 이전에 말했던 json 파일에 작성하는 함수입니다. 해당 함수에서 MCP 서버의 정보에 args에 포함되게 됩니다.
  • get_venv_path: 해당 함수는 아시다시피 가상환경의 path 정보를 가져오는 함수입니다.

이렇게 코드를 작성하고 RUN을 하게 된다면 아래의 사진과 같이 json파일이 수정되는 것을 볼 수 있습니다.

이렇게 코드는 간단합니다. 그렇게 어려운게 없었죠? 그럼 실행을 해야하는데 무조건 MCP를 설치하고 나서는 혹은 수정하고 나서는 Claude를 종료하고 다시 시작해야합니다. 안그러면 반영이 안되요.... 너무 번거로워....

📌 최종 실행 화면

만일 Claude에 MCP 코드가 제대로 들어갔다면 아래의 사진과 같이 도구가 추가된 것을 볼 수 있을 것입니다!



첫 번째 사진은 MCP서버가 정상적으로 등록이 된 것이고, 2번째 사진은 우리가 만들었던 Tools들이 정상적으로 등록이 된 것입니다!

도구가 2개인데 순서대로 말씀드리자면

  • search_location: 검색어의 위치를 찾는 것!
  • search_keyword: 검색어의 카테고리를 통해 검색하는 것(예. 병원, 식당....)

그럼 실행을 한번 해볼텐데 저는 현 위치 근처의 식당을 검색해달라고 해보겠습니다!

위와 같이 뜨는데요! 도구를 알아서 선택하고 쿼리의 파라미터 또한 알아서 입력해주는 것을 볼 수 있습니다. 그리고 만일 요청이 실패하면 알아서 해당 요청을 구체화하거나 파라미터를 변경하는 식으로 문제를 해결해나갑니다!

이번에는 카카오 본사의 위치를 물어봤는데 첫 번째에는 특정 키워드를 통해 검색하여 search_keyword 도구를 사용했지만 이번에는 특정 장소의 위치를 바로 물어봤기 때문에 search_location 도구를 사용하는 것을 볼 수 있었습니다!

나중에는 특정 위치의 키워드를 통해 검색을 하게 되면 두 가지 도구를 혼합해서 사용하기도 하는데 정말 LLM이 발전했다는 것을 볼 수 있었습니다~!

📌 마무리

지금까지 MCP가 무엇인지 알아보고 KAKAO MAP API를 활용해 MCP 서버를 만드는 실습을 진행했습니다.

저는 나중에 우리가 사용하고 있는 파이참, 인텔리제이와 같은 IDE에 이런 LLM이 상용화되고 주로 사용하고 있는 서비스(도커, 쿠버네티스, 모니터링 도구 등)이런 것들이 MCP 서버로 등록이 된다면 정말 앞으로 개발자 혼자 일하지 않고 LLM과 같이 일하는 환경이 올 것 같다는 생각이 들더라구요!

물론 나온지 얼마 안된 기술이라 섣불리 판단할 수는 없지만. 그래도 MCP라는 기술을 눈여겨 봐야겠다는 생각이 들었습니다. 그럼 다들 유용한 포스팅이었길 바라며 마치겠습니다👋

profile
하루에 한편 씩 읽기 좋은 테크 로그

0개의 댓글