Function Calling / Tool Calling / MCP JSON-RPC — 네 번의 왕복 / 스키마 방언 / 관측 가능성과 격리
Cursor, Claude Code 또는 Agent 프레임워크 디버그 로그를 열면 거의 모든 페이로드가 JSON입니다. 도구 카탈로그는 JSON Schema, 모델 tool_calls는 JSON, MCP Server 프레임은 JSON-RPC 2.0입니다. 이는 우연이 아닙니다 — Agent는 예측 불가능한 자연어와 실행 가능한 프로그램 인터페이스를 연결해야 하고, JSON은 양쪽이 안정적으로 파싱할 수 있는 교환 형식입니다.
이 글은 Function Calling → Tool Calling → MCP를 따라가며 완전한 도구 호출에서 네 번의 JSON 왕복을 매핑합니다. 클라우드 Mac에 MCP Server를 배포하거나 GitHub Agent 선정 목록에서 프레임워크를 고르는 중이라면, API 문서를 외우는 것보다 이 흐름을 이해하는 편이 디버깅에 더 낫습니다.
Agent는 거의 모든 곳에서 JSON을 사용한다
LLM은 토큰 스트림을 출력하고, Shell·데이터베이스·HTTP API는 구조화된 인자가 필요합니다. 초기 관행은 프롬프트에 「이 JSON 형식으로 답해」라고 쓰는 것이었습니다 — 모델은 trailing comma를 넣거나 따옴표를 빼거나 Markdown fence로 감싸고, 파서는 계속 실패했습니다.
2023년 이후 주요 API는 구조화된 액션을 프로토콜 계층으로 옮겼습니다: 모델이 임의의 JSON 텍스트를 자유롭게 출력하지 않고, 서버가 출력 형태를 제한하고 Host가 실행합니다. 파이프라인은 다음과 같습니다:
- Declare — JSON Schema로 도구 등록;
- Decide — 제한된 문법 안에서 도구 이름과 파라미터 결정;
- Execute — 로컬 코드 또는 MCP Server로 실행;
- Write back — 결과를 JSON 메시지로 컨텍스트에 되돌려 쓰기.
설계 원칙: Agent 관측 가능성 = 모든 JSON 단계를 로깅·재생·diff. 구조화된 중간 단계 없이는 자동화를 감사할 수 없습니다.
Function Calling: 자연어에서 구조화된 액션으로
OpenAPI 시대의 function 객체
OpenAI는 Chat Completions에 functions / tools를 추가했습니다. 요청에 도구 정의를 붙이면 모델은 regex로 파싱해야 하는 텍스트 대신 function_call 또는 tool_calls를 반환합니다.
일반적인 요청 스니펫
{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "도쿄 날씨?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시별 현재 날씨",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}]
}프롬프트의 수기 JSON 템플릿은 프로토콜 제약으로 대체되고, Host는 arguments를 스키마로 검증한 뒤 실제 함수를 호출합니다. Function Calling은 모델이 말하는 것을 고정합니다 — 도구가 어디서 실행되는지, 어떻게 발견되는지, 인증이 어떻게 되는지는 아닙니다. Tool Calling 생태계와 MCP가 그것을 담당합니다.
Tool Calling: 턴당 네 번의 JSON 왕복
- Register — Host가 JSON Schema가 포함된
tools[]를 모델 API에 전송; - Decide — API가
id,name,arguments(JSON 문자열)를 포함한tool_calls반환; - Execute — Host가 인자를 파싱하고 로컬 코드 또는 MCP 호출;
- Write back —
role: tool메시지가 결과 반환, 모델이 사용자용 답변 생성.
{
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Tokyo\"}"
}
}]
}arguments는 중첩 객체가 아니라 문자열입니다 — 스트리밍과 SDK 호환을 위해. Host는 검증 전에 JSON.parse(arguments)를 실행해야 합니다. DevTools에서 Ctrl + Shift + I를 누르면 Network 탭에서 같은 구조를 볼 수 있습니다.
| 단계 | 방향 | 페이로드 | 파서 |
|---|---|---|---|
| ① Register | Host → API | tools[] + JSON Schema | API 게이트웨이 |
| ② Decide | API → Host | tool_calls | Host / SDK |
| ③ Execute | Host → Tool | 파싱된 객체 | 도구 구현 |
| ④ Write back | Host → API | {role:"tool", content:"..."} | 모델 |
API 스키마 방언
- OpenAI / 호환 엔드포인트
tools+tool_choice; 스트리밍은delta.tool_calls청크 사용.- Anthropic
input_schema가 있는tools; 응답은tool_use블록과tool_result회신.- Google Gemini
- 필드명이 다른
functionDeclarations와functionCall. - Ollama 로컬
- OpenAI 호환
tools; 기능은 모델 학습에 따라 다름.
LangGraph, DeepSeek Harness 같은 프레임워크(우리 Harness 가이드 참고)는 이런 방언을 하나의 Tool 추상화로 압축합니다 — 아래는 여전히 JSON입니다.
MCP: JSON-RPC가 도구 계층을 표준화
Model Context Protocol (MCP)는 Function Calling 아래에 있습니다 — 도구 프로세스가 Host 간에 어떻게 발견·파라미터화·재사용되는지. 전송은 stdio 또는 SSE일 수 있지만 메시지는 항상 JSON-RPC 2.0입니다.
| 계층 | 프로토콜 | 예시 |
|---|---|---|
| Model API | REST / OpenAI 호환 | chat/completions + tools |
| MCP | JSON-RPC 2.0 | tools/list, tools/call |
| Business | 임의 | 파일, DB, HTTP |
Host는 시작 시 tools/list를 호출하고, 카탈로그를 API tools[]로 변환한 뒤, 모델이 결정한 후 tools/call을 전송합니다 — 두 번의 방언 변환, 하나의 의미적 파이프라인.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": { "path": "/tmp/demo.txt" }
}
}stdio 모드에서 JSON 프레임은 어떻게 구분되나요?
MCP stdio는 newline 구분 JSON이 아니라 Content-Length 헤더 + JSON 본문(LSP와 유사)을 사용합니다. 큰 페이로드(예: base64 리소스)도 한 프레임에 들어갑니다. SSE는 HTTP 이벤트 스트림으로 JSON-RPC를 감싸 멀티 클라이언트 Server를 지원합니다.
전체 데이터 흐름: 사용자 입력부터 도구 실행까지
- 로그가 각
tools/list버전과 도구 수를 기록하는가? - 실행 전에
arguments가 스키마로 검증되는가? - MCP 타임아웃이 읽기 쉬운
tool메시지로 매핑되는가? - 로깅 전에 비밀 정보가 제거되는가?
우리 AI Coding Workflow의 Verify 단계와 같은 아이디어입니다 — 파싱 가능한 중간 단계 없이는 Agent가 올바른 도구를 실제로 호출했는지 자동 검증할 수 없습니다.
Protobuf나 XML은 왜 아닐까?
- 모델 API는 JSON 우선; 바이너리는 직렬화 계층을 추가;
- 디버깅 — 엔지니어가 로그에서 파라미터를 읽음;
- 스키마 생태계 — JSON Schema와 OpenAPI가 사실상 표준;
- 스크립트와 브라우저 — curl, n8n, 프론트엔드 데모는 JSON을 가정.
트레이드오프: 크기와 CPU. High-QPS 설정은 내부 캐시와 로그 압축을 사용하고, 큰 blob은 JSON 필드 안에 base64 — JSON envelope을 대체하지 않습니다.
JSON 비용과 엔지니어링 대응
| 증상 | 원인 | 대응 |
|---|---|---|
| arguments parse error | 모델의 잘못된 JSON 문자열 | 더 엄격한 스키마, 낮은 temperature, 재시도 |
| 잘못된 도구 | 모호하거나 너무 많은 도구 설명 | 도구 병합, 부정 예시 추가 |
| MCP 멈춤 | 불완전한 stdio 프레임 또는 죽은 프로세스 | launchd 감독, 헬스 체크 |
| 로그에 비밀 노출 | 도구 결과에 환경 변수 포함 | 결과 redact, 별도 실행 계정 |
프로덕션 팁: 격리된 클라우드 Mac에서 MCP 실행 — 노트북의 Cursor가 SSH로 JSON-RPC를 전송하고, Shell은 일상 기기에 두지 않습니다.
Agent가 JSON에 의존하는 이유는 자연어 결정과 프로그램 실행이 사람이 읽을 수 있고 기계가 검증할 수 있으며 플러그 가능한 계약이 필요하기 때문입니다. Function Calling은 모델이 말하는 것을 정의하고, Tool Calling은 Host가 한 턴을 어떻게 실행하는지 정의하고, MCP는 도구 프로세스가 생태계에 어떻게 연결되는지 정의합니다 — JSON은 세 가지를 잇는 실입니다.
클라우드 Mac에서 MCP의 JSON 실행 계층 운영
M4 전용 노드, 일 단위 과금, SSH 즉시 사용
싱가포르 · 일본 · 한국 · 홍콩 · 미국 리전
격리된 클라우드 Mac에서 tools/call을 실행하고, 로컬 Host는 JSON-RPC만 전송합니다. ZekVPS 클라우드 Mac mini 플랜 보기