본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 29. 02:35

Django를 Claude에 연결하기: OpenAPI로부터 MCP 서버 생성하기

요약

Django API의 OpenAPI 스키마를 Model Context Protocol(MCP) 서버로 자동 변환하여 LLM 에이전트가 API를 직접 조작할 수 있게 만드는 방법을 소개합니다. 수동 도구 작성의 유지보수 문제를 해결하고 기존 스키마를 활용하는 엔지니어링 접근법을 다룹니다.

핵심 포인트

  • OpenAPI 스키마를 활용해 MCP 도구를 자동 생성함으로써 유지보수 비용 절감
  • drf-spectacular를 이용한 Django API의 MCP 서버 변환 과정 설명
  • 단순 연결을 넘어 실제 에이전트 배치 시 발생하는 엔지니어링 문제 고찰
  • 기존 오픈소스 패키지들의 작동 원리 이해의 중요성 강조

만약 당신이 Django API를 구축한다면, 아마도 OpenAPI 스키마를 생성하기 위해 drf-spectacular를 사용하고 있을 것입니다. 해당 스키마는 이미 당신이 가진 모든 엔드포인트(endpoint)를 설명하고 있습니다. 여기서는 이를 Model Context Protocol (MCP) 서버로 자동 변환하는 방법과, 실제 AI 에이전트가 이를 호출하기 시작할 때 발생하는 더 어려운 엔지니어링 문제들에 대처하는 방법을 소개합니다.

만약 당신이 LLM이 단순히 Django API에 대해 이야기하는 것을 넘어, 실제로 API를 _조작(operate)_하기를 원한다면, LLM에게 도구(tools)를 제공해야 합니다. 가장 단순한 방법은 엔드포인트당 하나의 MCP 도구를 직접 작성하는 것입니다: 이름, 설명, 입력 스키마(input schema), 그리고 호출 자체를 말이죠. 이 방식은 첫날에는 작동하지만, 30일이 지나면 부패합니다. 새로운 엔드포인트가 생길 때마다, 필드 이름이 바뀔 때마다, 쿼리 파라미터(query param)가 변경될 때마다, 당신은 이를 업데이트해야 한다는 사실을 반드시 기억해야 하는 두 번째 작업이 생깁니다. 유지보수 비용은 API의 표면적(surface)과 함께 증가하며, 이는 바로 당신이 LLM이 길들여주길 바랐던 바로 그 부분입니다.

하지만 당신은 이미 전체 API를 한 번 설명해 두었습니다. 만약 drf-spectacular를 사용하고 있다면, 그리고 2026년에 DRF를 사용하고 있다면(거의 확실히 사용하고 있겠지만), 당신의 프로젝트에는 이미 모든 경로(path), 파라미터(parameter), 타입을 알고 있는 OpenAPI 3 스키마가 놓여 있습니다. 그 스키마가 바로 당신의 도구 정의(tool definitions)가 될 수 있습니다. 당신은 그저 무언가에게 이를 읽는 법을 가르치기만 하면 됩니다.

DRF views (drf-spectacular) ──▶ OpenAPI schema ──▶ MCP tools ──▶ Claude (또는 다른 모든 LLMs)

이 포스트는 두 부분으로 나뉩니다. 첫 번째는 연결(wiring)입니다: 기성 도구들이 내부적으로 무엇을 하는지 이해할 수 있도록, 실제 코드를 사용하여 해당 스키마를 작동하는 MCP 서버로 변환하는 방법을 다룹니다. 그다음은 더 어려운 부분입니다: OpenAPI와는 상관없고, 해당 도구들 앞에 실제 에이전트를 배치하는 것과 직결된 문제들을 다룹니다.

먼저, 솔직하게 말씀드리자면: 이것은 새로운 것이 아닙니다

OpenAPI 명세(spec)로부터 LLM 도구(tools)를 생성하는 것은 이미 해결된 문제입니다. Django 측면에서는 django-rest-framework-mcp와 같은 패키지들이 거의 정확히 이 작업을 수행합니다. 이들은 여러분의 DRF 설정에 직접 연결되어 최소한의 설정만으로 View와 ViewSet을 MCP 도구로 노출합니다. 또한 범용적인 openapi → mcp 컨버터(openapi-to-mcp, openapi-mcp-generator)도 있으며, FastMCP는 네이티브 OpenAPI 지원을 제공합니다.

그렇다면 왜 이 글을 계속 읽어야 할까요? 만약 여러분이 단순히 저 중 하나를 pip install만 한다면, 그것이 내부적으로 무엇을 하고 있는지 결코 배우지 못할 것이기 때문입니다. 그것이 여러분의 API와 완벽하게 맞지 않는 날이 오면, 여러분은 블랙박스(black box)를 디버깅하느라 막히게 될 것입니다. 저는 여러분이 모든 조각을 볼 수 있도록 작고 읽기 쉬운 참조 구현체(reference implementation)를 만들었습니다. 유지 관리되는 의존성(dependency)을 원한다면 FastMCP를 사용하세요. 그것이 어떻게 작동하는지 이해하고 싶다면, 계속 읽어주세요.

파트 1: 다섯 가지 구성 요소로 이루어진 배선(wiring)

1. 조사(Introspect) (프로세스 내부에서 수행, HTTP 셀프 호출 없음). 본능적으로는 HTTP를 통해 /api/schema/를 가져오고 싶겠지만, 그러지 마세요. drf-spectacular는 실행 중인 서버 없이도 프로세스 내부에서 동일한 스키마를 제공합니다:

from drf_spectacular.generators import SchemaGenerator

generator = SchemaGenerator()
...

그게 전부입니다. 모든 엔드포인트(endpoint)를 설명하는 일반적인 딕셔너리(dict)가 생성됩니다. (만약 스키마가 원격 서비스나 비-DRF API 등 다른 곳에 있다면, URL을 가져와서 직접 $ref 포인터를 해석하는 방식으로 돌아가야 합니다.)

2. 도구 명세(tool specs) 생성. 스키마의 paths를 순회하며 각 연산(operation)을 도구로 변환합니다. operationId는 이름이 되고, 파라미터(parameters)는 JSON 스키마(JSON Schema)가 됩니다. 경로 파라미터(Path params)는 항상 필수이며, 쿼리 파라미터(query params)는 자체 플래그를 따릅니다:

for param in operation.get("parameters", []):
    if "$ref" in param:                         # 파라미터는 참조(reference)일 수 있습니다
        param = resolve_ref(schema, param["$ref"])
...

정상적인 경로(happy-path)를 다루는 튜토리얼들이 생략하는 두 가지 주의사항(gotchas)이 있습니다.

첫째, $ref 해결 (resolution): 파라미터가 인라인 객체 대신 {"$ref": "#/components/parameters/Foo"}와 같은 형태로 전달될 수 있습니다. 포인터를 해결하지 않으면 망가진 도구(tools)를 생성하게 됩니다.

둘째, 이름 충돌 및 문자 집합 (name collisions and charset): MCP 도구 이름은 제한된 문자 집합만 허용하며, 두 개의 작업(operations)이 동일한 operationId를 공유할 수 있습니다. 이를 정제(sanitize)하고 중복을 제거하지 않으면 클라이언트가 도구 목록 전체를 거부합니다.

3. 서빙(Serve): 가장 중요한 단 하나의 MCP 결정 사항. 공식 mcp Python SDK는 도구를 정의하는 두 가지 방법을 제공합니다. 상위 수준(high-level)의 FastMCP @tool 데코레이터는 작성 시점에 이미 알고 있는 도구들을 만들 때 유용합니다. 하지만 이 작업에는 적합하지 않은 도구입니다. 우리는 *런타임(runtime)*에 도구를 생성하며, 각 도구는 사전에 알 수 없는 JSON Schema를 가집니다. 이 경우에는 하위 수준(low-level)의 Server를 사용해야 합니다:

from mcp.server.lowlevel import Server
import mcp.types as types

...

이 포스트의 전반부에서 한 가지만 기억해야 한다면 바로 이것입니다: @tool은 정적(static) 도구를 위한 것입니다. 하위 수준의 Server는 생성된 도구를 위한 것입니다. 데코레이터를 먼저 사용하려다가 나중에 이를 해결하느라 고생하는 것이 가장 흔한 실수입니다.

4. 실행(Execute). 도구 명세(tool spec)는 실제로 두 가지로 구성됩니다: 광고할 스키마(schema)와 호출을 수행하기 위한 라우팅 정보(routing info)입니다. 호출 시점에 인자(arguments)를 다시 분리합니다. 경로 파라미터(path params)는 URL 템플릿에 치환되고, 쿼리 파라미터(query params)는 쿼리 문자열로 첨부됩니다. 그런 다음 요청을 보냅니다:

for key, value in arguments.items():
    if key in path_params:
        path = path.replace("{" + key + "}", str(value))
...

5. 두 가지 전송 방식(transports), 하나의 서버. 동일한 Server 객체는 stdio(Claude Desktop 및 Claude Code가 로컬 서버를 실행하는 방식)와 Streamable HTTP(배포된 서버를 위한 방식)를 통해 서비스를 제공합니다.

제가 처음 시도했을 때 한 시간을 허비하게 만든 주의사항이 하나 있습니다: stdio 모드에서는 stdout이 곧 프로토콜 채널입니다. 잘못된 print() 호출 하나가 스트림을 오염시키면, 클라이언트는 도구가 하나도 없다는 것을 조용히 표시합니다.🙃

대신 항상 진단 로그(diagnostics)를 stderr로 기록하세요.

의식적으로 결정해야 할 두 가지 사항

기본적으로 안전하게 (Safe by default). 오직 GET 엔드포인트만 도구(tool)가 됩니다. DELETE를 포함하여 전체 CRUD 인터페이스를 LLM에 자동으로 노출하는 것은, 에이전트가 운영 환경의 데이터를 삭제했을 때 팀원들에게 그 이유를 설명해야 하는 상황을 초래합니다. 쓰기 작업은 엄격하게 선택 사항(opt-in)입니다 (INCLUDE_METHODS = ["GET", "POST"]). 패키지의 기본 설정을 확인하고, 실수로가 아니라 의도적으로 이를 재정의하세요.

전문가 팁: 안전한 GET 요청 내에서도, LLM이 근처에도 오지 않기를 바라는 뷰(view)들이 분명히 있을 것입니다. 무거운 리포팅 엔드포인트, 결제 집계, 혹은 비용이 많이 드는 작업 등이 그렇습니다. 스키마가 프로세스 내에서 생성되기 때문에, drf-spectacular의 네이티브 @extend_schema(exclude=True) 데코레이터를 사용하여 이를 숨길 수 있습니다. 이 데코레이터를 사용하면 OpenAPI 문서와 생성된 도구 목록 모두에서 해당 엔드포인트를 한 번에 제거할 수 있습니다.

인증(Auth): 까다로운 부분. 생성된 도구가 인증을 할 수 없다면 무용지물입니다. 따라서 자격 증명(credentials)은 한 번 설정되면(token / bearer / custom header) 모든 나가는 요청에 적용됩니다. 하지만 한계점에 대해 솔직히 말씀드리자면, 이 방식은 정적(static) 자격 증명을 사용합니다.

공유 서비스 계정 대신 호출하는 사용자 본인의 자격 증명을 전달하여 각 도구가 해당 사용자의 권한으로 실행되도록 하는 더 나은 버전은, 데모와 실제 배포를 가르는 경계선입니다. 이 튜토리얼을 포함한 그 어떤 튜토리얼도, 단일 정적 토큰을 가리키고 있으면서 인증이 "완료되었다"고 말하게 두지 마세요.

직접 시도해보기

이 리포지토리에는 실행 가능한 example/ DRF 프로젝트인 작은 상점(상품 + 주문, in_stock 필터 포함)이 포함되어 있어, 약 2분 만에 전체 루프가 작동하는 것을 확인할 수 있습니다:

git clone https://github.com/Shanahan-Suresh/django-openapi-mcp
cd django-openapi-mcp
install "git+https://github.com/Shanahan-Suresh/django-openapi-mcp"

Django settings.py에서

INSTALLED_APPS = [..., "rest_framework", "drf_spectacular", "django_openapi_mcp"]
DJANGO_OPENAPI_MCP = {"BASE_URL": "http://127.0.0.1:8000"}

새 터미널에서 실행

python manage.py run_mcp_server --transport stdio

Claude Desktop의 claude_desktop_config.json에 연결하고 재시작하면, 모든 엔드포인트(endpoints)가 여러분만의 MCP 서버 내 도구(tools)가 됩니다:

Claude Desktop connecting to the MCP server

이제 재미있는 부분입니다. 채팅창을 열고 Claude에게 여러분의 Django 앱과 상호작용하도록 요청해 보세요: "재고가 있는 모든 상품을 나열해줘."

Claude는 여러분의 프롬프트를 products_list 도구에 자동으로 매핑하고, 스키마(schema)를 바탕으로 in_stock=true 쿼리 파라미터(query parameter)를 전달해야 함을 이해한 뒤, 실행 권한을 요청할 것입니다:

Claude asking for permission to use the Products list tool

권한을 허용하면, 도구가 로컬 Django API를 호출하여 JSON을 반환하고, Claude는 이를 깔끔한 표 형식으로 구성합니다. 여러분은 직접 작성한 글루 코드(glue code) 없이 LLM(대규모 언어 모델)에 데이터베이스 읽기 권한을 부여한 것입니다.

Claude displaying the in-stock products table

파트 2: 도구 생성은 쉬운 부분입니다

위의 모든 과정은 오후 한나절이면 끝나는 작업이며, 여러 패키지들이 이를 대신 해줄 것입니다. 그다음에는 실제 에이전트(agent)를 해당 도구들에 연결하고 다단계 작업을 요청해 보세요: "이 고객의 가장 최근 주문을 찾아서 그 안에 있는 모든 품목이 여전히 재고가 있는지 알려줘."

여러분은 도구가 결코 어려운 부분이 아니었음을 깨닫게 될 것입니다. 진짜 어려운 부분은 모델과 도구 사이에 존재하는 모든 것입니다.

실제 배포 환경에서 에이전트는 보통 채팅 인터페이스, 즉 누군가가 질문을 입력하고 도구 호출 (tool calls) 결과가 스트리밍되는 것을 지켜보는 얇은 웹 UI 뒤에 위치합니다. 이러한 표면적인 환경이 아래에서 언급할 문제들을 변화시키지는 않습니다. 호출자가 채팅 앱이든, Claude Desktop이든, 혹은 크론 잡 (cron job)이든 문제는 동일합니다.

생성된 도구는 '선택 가능한' 도구가 아닙니다. 가공되지 않은 스키마 (raw schema)는 각 작업(operation)당 하나의 도구를 제공하며, 그 이름은 operationId를 따릅니다. 문서를 읽는 사람에게는 괜찮을지 몰라도, 어떤 도구를 사용할지 결정해야 하는 모델에게는 거의 쓸모가 없습니다. 사용자가 "이 고객의 마지막 주문이 여전히 이행 가능한가요?"라고 물었을 때, orders_list, orders_retrieve, products_retrieve 중 어떤 것이 실행되어야 하며, 어떤 순서로 실행되어야 할까요?

결국 여러분은 각 도구가 무엇을 필요로 하고 무엇을 산출하는지에 대한 인덱스인 의미론적 풍부화 (semantic enrichment) 계층을 얇게 추가하게 됩니다. 이를 통해 에이전트는 단순히 도구 이름을 패턴 매칭하는 대신, 기능에 대해 추론(

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0