[MCP] 코드 레벨에서 뜯어보는 Agent Tool Calling 분석
이번에는 AI가 어떻게 MCP라는 도구를 사용해서 한층 더 업그레이드 된 답변을 주는지, 타 시스템과 연동을 하는지, 결과적으로 AI Agent로 변화하는 과정에 대해 알아볼 예정이다. 사용자가 요청을 했을때, 백엔드단에서 일어나는 과정을 상세히 알기 위해 코드로 구현해보며 동작 과정을 살펴보았다.
Agent Tool Calling 과정 분석
수행을 하는 주체를 크게 보면 4개가 있다.
- 호스트 앱
- Claude Desktop, Cursor, ChatGPT 앱, 커스텀 Agent 서버 등
- LLM
- Gemini, ChatGPT, Claude 등
- MCP Client
- Host 내부 컴포넌트
- 보통 MCP 서버 1개당 클라이언트 1개
- initialize, tools/list, tools/call 같은 MCP 메시지를 보냄
- MCP Server
- 실제 도구를 제공하는 서버
- github, database, browser, korean-law-mcp
여기서 중요한점은 LLM은 “도구를 쓰겠다”만 결정하고, 실제 실행과 연결은 전부 Host + MCP Client가 담당한다는 점이다. 꼭 기억해야 하는 포인트다.
상세 흐름도
1. Host가 MCP Client를 만들고
↓
2. MCP Client가 MCP Server에서 tool schema를 가져오고
↓
3. Host가 그 schema를 LLM에게 제공하고
↓
4. LLM은 tool_call만 만들고
↓
5. Host가 MCP Client를 통해 실제 tools/call을 실행하고
↓
6. 결과를 다시 LLM에게 넣고
↓
7. LLM이 최종 답변을 만든다.
이제 이 내용을 코드로 만들어 실제 동작 과정을 눈으로 확인해보자.
프로젝트 구성
간단하게 Python, openAI, MCP(커스텀, 외부)를 사용했다. 서버가 기동되고 MCP 연결 > 채팅 > 최종 결과 답변까지 전체 플로우를 확인하기 위해 디버그 로그를 넣었다.
디렉토리 구조
- agent.py: 에이전트
- cli.py: 입력 제어
- mcp_server.py: MCP 서버
시작 지점
시작점은 demo_mcp_agent/__main__.py이다.
여기서 바로 cli.py의 main()으로 넘어간다.
즉 실제 시작 흐름은 아래와 같다.
1
2
3
4
python -m demo_mcp_agent
-> __main__.py
-> cli.main()
-> cli.async_main()
CLI에서 하는 일
CLI에서는 우선 .env를 읽고, 모델과 API 키를 준비한다.
그 다음 MCPToolAgent를 만든다.
OPENAI_API_KEY가 있으면 일반 채팅까지 가능하다.
없으면 LLM 호출은 하지 않고 /tools, /call만 사용할 수 있다.
이렇게 해두면 OpenAI 설정이 없어도 MCP 연결 자체는 테스트할 수 있다.
MCP 서버 연결
Agent의 실질적인 부팅은 start()에서 일어난다.
현재 연결 대상은 두 가지다.
local항상 연결되는 필수 MCP 서버이다.demo_mcp_agent.mcp_server를 실행한다.law선택 MCP 서버이다.LAW_OC가 있거나KOREAN_LAW_MCP_ENABLED=true이면 연결을 시도한다.
law 서버는 실패해도 전체 프로그램을 죽이지 않는다. 경고만 남기고 로컬 MCP만 계속 사용한다.
그리고 law 서버의 도구들은 law__ prefix를 붙여 노출한다. 예를 들어 외부 MCP의 search_law 도구는 이 프로젝트 안에서는 law__search_law로 보인다.
도구 목록 조회와 등록
MCP 서버와 연결되면 initialize() 후 list_tools()로 도구 목록을 가져온다.
가져온 도구는 그대로 쓰지 않고 RegisteredTool로 감싸서 registry에 넣는다.
여기서 중요한 건 이름이 두 개라는 점이다.
public_nameCLI와 OpenAI가 보는 이름이다. 예:law__search_lawsource_name실제 MCP 서버에 존재하는 도구 이름이다. 예:search_law
이렇게 분리해두면 외부 MCP 서버가 여러 개 붙어도 이름 충돌을 피할 수 있다.
OpenAI 도구 목록 만들기
MCP 도구는 OpenAI에 그대로 넘길 수 없다. OpenAI function tool schema 형태로 바꿔야 한다.
결국 모델에게는 “너는 이런 도구를 쓸 수 있다”는 목록을 넘겨주는 것이다.
모델은 이 목록을 보고 도구를 쓸지 말지 결정한다.
사용자 입력 분기
부팅이 끝나면 CLI는 사용자 입력을 받는다.
입력은 크게 세 갈래다.
/tools현재 등록된 MCP 도구 목록을 출력한다./callLLM 없이 MCP 도구를 직접 호출한다.- 일반 입력 OpenAI Responses API를 거치는 일반 tool-calling 흐름으로 들어간다.
/call 직접 호출
/call은 LLM을 아예 거치지 않는다.
실제 실행은 registry를 통해 적절한 MCP session으로 라우팅된다.
사용 예시는 아래와 같다.
1
2
/call calculator {"operation":"add","left":7,"right":11}
/call law__search_law {"query":"근로기준법"}
이 경로는 MCP 연결과 도구 동작을 확인할 때 유용하다.
LLM 문제인지 MCP 문제인지 분리해서 볼 수 있기 때문이다.
일반 채팅 흐름
일반 입력은 agent.chat(raw)로 들어간다.
첫 번째 모델 호출에서는 사용자 메시지와 도구 목록을 같이 보낸다.
모델 응답에 function_call이 없으면 바로 최종 답변이다.
반대로 function_call이 있으면 Agent가 MCP 도구를 실행하고, 도구 결과를 다시 모델에게 넘긴다.
그 다음 모델이 도구 결과를 반영해서 최종 자연어 답변을 만든다.
즉 일반 채팅의 실제 구조는 아래와 같다.
1
2
3
4
5
사용자 질문
-> 모델이 도구 필요 여부 판단
-> 필요하면 Agent가 MCP 도구 실행
-> 실행 결과를 다시 모델에게 전달
-> 최종 답변 생성
로컬 MCP 서버
로컬 MCP 서버는 demo_mcp_agent/mcp_server.py이다.
현재 로컬 도구는 두 개다.
서버는 stdio 방식으로 실행된다.
로컬 방식이라 별도 웹 서버를 띄우는 구조가 아니다. Agent가 Python subprocess로 MCP 서버를 실행하고, 표준 입출력으로 통신한다.
실제 구동 로그
여기서는 실제로 프로젝트를 실행했을 때 커맨드창에 찍히는 로그를 정리한다.
실행 조건은 다음과 같다.
OPENAI_API_KEY는.env에서 읽었다.- law MCP는 끄고 로컬 MCP만 연결했다.
- 입력은
23 곱하기 9 계산해줘,그냥 안녕이라고만 답해줘,/quit순서로 넣었다.
흐름은 크게 세 덩어리로 보면 된다.
- 부팅 및 채팅 준비
- 일반 채팅 처리
- MCP tool이 호출되는 경우
- MCP tool이 호출되지 않는 경우
- 종료 및 정리
흐름 1. 부팅 및 채팅 준비
프로그램이 시작되면 .env를 읽고, Agent를 만든 뒤 로컬 MCP 서버를 subprocess로 실행한다.
그 다음 initialize()와 list_tools()를 거쳐 도구 목록을 가져온다.
여기서 local 도구 2개 연결 완료와 OpenAI function tool schema 준비 완료가 찍히면, 모델에게 넘길 도구 목록까지 준비된 상태다.
그 다음 CLI가 사용 가능한 도구 목록과 LLM 사용 가능 여부를 출력한다.
여기서는 OPENAI_API_KEY가 감지되었기 때문에 일반 채팅 경로까지 들어갈 수 있다.
흐름 2. 일반 채팅 처리
이제부터가 실제 사용자가 채팅을 입력했을 때의 흐름이다.
둘 다 일반 채팅 경로 선택으로 시작하지만, 모델 응답에 function_call이 있느냐에 따라 갈라진다.
케이스 A-1. 로컬 MCP tool이 호출되는 채팅
사용자가 23 곱하기 9 계산해줘를 입력한 케이스다.
흐름을 보면 다음 순서로 진행된다.
[ROUTE] 일반 채팅 경로 선택[LLM] 첫 모델 호출 시작- 모델이
calculator도구 호출 요청 - Agent가
calculator -> local.calculator로 MCP 도구 실행 - 도구 결과
207.0을 받음 - 도구 결과를 반영하기 위해 두 번째 모델 호출
- 최종 답변 출력
즉 이 케이스에서는 모델이 직접 계산한 것이 아니라, calculator MCP tool을 호출해서 결과를 받은 뒤 최종 답변을 만든다.
케이스 A-2. korean-law-mcp가 호출되는 채팅
이번에는 law MCP를 켠 상태에서 관세법 제38조 알려줘를 입력한 케이스다.
이 케이스에서는 모델이 먼저 law__search_law를 선택해 관세법을 찾고, 이어서 law__get_law_text로 제38조 본문을 조회한다.
Agent 내부에서는 이 public name들을 각각 실제 MCP 서버의 law.search_law, law.get_law_text 호출로 라우팅한다.
흐름만 보면 calculator 케이스와 구조는 같다.
[LLM] 첫 모델 호출 시작- 모델이
law__search_law도구 호출 요청 - Agent가
law__search_law -> law.search_law로 MCP 도구 실행 - 검색 결과를 반영한 뒤 모델이 다시
law__get_law_text도구 호출 요청 - Agent가
law__get_law_text -> law.get_law_text로 관세법 제38조 본문 조회 - 최종 답변 출력
즉 도구가 calculator이든 korean-law-mcp이든, Agent 입장에서는 등록된 public_name을 찾아 해당 MCP session으로 실행한다는 구조는 동일하다.
케이스 B. MCP tool이 호출되지 않는 채팅
사용자가 그냥 안녕이라고만 답해줘를 입력한 케이스다.
여기서는 첫 모델 호출 뒤 바로 모델이 추가 도구 호출 없이 최종 답변 반환 로그가 찍힌다.
즉 function_call이 없었고, MCP tool 실행도 없었다.
흐름 3. 종료 및 정리
마지막으로 /quit을 입력하면 CLI 입력 루프를 빠져나가고 MCP 세션을 정리한다.
MCP 세션 종료 완료까지 찍히면 subprocess와 session 정리까지 끝난 것이다.
마무리
이제 MCP가 어떤 순서로 로드되고, 모델이 어느 지점에서 tool call을 요청하며, 실제 실행이 어디서 일어나는지 어느 정도 실체가 잡혔다. 결국 핵심은 모델이 직접 실행하는 것이 아니라, Agent가 MCP 서버와 연결된 도구를 대신 실행하고 그 결과를 다시 모델에게 넘긴다는 점이다.
다음에는 이미 만들어진 MCP를 붙여 쓰는 수준을 넘어, 직접 MCP 서버를 만들어볼 예정이다. 도구를 어떤 단위로 나누고 input schema를 어떻게 설계해야 하는지 직접 구현해보면 MCP 구조가 더 명확해질 것 같다.

























