Claude 프로그래매틱 도구 호출로 모델 왕복 줄이기
Claude의 프로그래매틱 도구 호출과 코드 실행 컨테이너 구조, allowed_callers 설정, 에러 처리와 DB 연동까지 실무 관점으로 정리한 가이드
2026-08-12 · 최초 발행 2026-04-26
프로그래매틱 도구 호출이 바꾸는 것
기존 도구 호출 방식에서는 Claude가 도구를 부를 때마다 모델↔API↔도구 사이를 왕복한다. 프로그래매틱 도구 호출은 이 왕복을 코드 실행 컨테이너 안으로 밀어 넣는다. Claude가 컨테이너 내부에서 Python 코드로 도구를 직접 호출하면서 지연 시간을 줄이고(다중 도구 호출의 왕복 최소화), 토큰을 절약하고(중간 결과를 처리한 뒤 요약만 반환), 루프·조건문·데이터 필터링 같은 복잡한 로직까지 처리한다.
[기존 방식 - 5번의 왕복]
사용자 → Claude → 도구1 → Claude → 도구2 → Claude → 도구3 → Claude → 도구4 → Claude → 도구5 → Claude → 응답
[프로그래매틱 방식 - 코드 내에서 처리]
사용자 → Claude → [컨테이너에서 도구1~5 순차 호출] → Claude → 응답
코드는 사용자 서버가 아니라 Anthropic이 관리하는 샌드박스에서 돈다. 로컬 환경과 비교하면 격리 수준과 상태 유지 방식이 다르다.
| 구분 | 로컬 환경 | Claude 컨테이너 |
|---|---|---|
| 실행 위치 | 사용자 서버/PC | Anthropic 관리 샌드박스 |
| 보안 | 직접 관리 필요 | 격리된 환경 제공 |
| 상태 유지 | 영구적 | 약 4.5분 후 만료 |
| 네트워크 | 제한 없음 | 제한적 |
컨테이너는 Claude가 Python 코드를 실행하는 일시적인 가상 작업 공간이다. 도커 컨테이너와 비슷하게 격리되어 있고, container_id로 세션 간 재사용도 가능하지만 비활성 상태로 4.5분이 지나면 자동으로 사라진다.
┌─────────────────────────────────────────────────────────────┐
│ Anthropic 클라우드 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 코드 실행 컨테이너 │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Python 환경 │ │ │
│ │ │ - 도구를 함수로 호출 │ │ │
│ │ │ - 데이터 처리/필터링 │ │ │
│ │ │ - 결과 집계 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ container_id: "container_xyz789" │ │
│ │ expires_at: "2025-01-15T14:30:00Z" │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
필수 설정
베타 헤더를 빠뜨리면 기능 자체가 동작하지 않는다.
betas=["advanced-tool-use-2025-11-20"]
지원 모델과 도구 버전은 다음과 같다.
| 모델 | 모델 ID | 도구 버전 |
|---|---|---|
| Claude Opus 4.5 | claude-opus-4-5-20251101 |
code_execution_20250825 |
| Claude Sonnet 4.5 | claude-sonnet-4-5-20250929 |
code_execution_20250825 |
필요한 패키지는 하나뿐이다.
pip install anthropic
실무 예제로 보는 패턴
가장 기본이 되는 구조는 code_execution 도구와 사용자 정의 도구를 함께 등록하고, 사용자 정의 도구에 allowed_callers로 코드 실행에서만 호출되도록 못 박는 것이다.
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-5",
betas=["advanced-tool-use-2025-11-20"],
max_tokens=4096,
messages=[{
"role": "user",
"content": "서울, 부산, 대구 지역의 매출 데이터를 조회하고 최고 매출 지역을 알려줘"
}],
tools=[
# 1. 코드 실행 도구 활성화 (필수)
{
"type": "code_execution_20250825",
"name": "code_execution"
},
# 2. 프로그래매틱 호출 가능한 사용자 정의 도구
{
"name": "query_database",
"description": "SQL 쿼리를 실행합니다. JSON 객체 리스트를 반환합니다.",
"input_schema": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "실행할 SQL 쿼리"}
},
"required": ["sql"]
},
# 핵심: 코드 실행에서만 호출 가능하도록 설정
"allowed_callers": ["code_execution_20250825"]
}
]
)
Claude가 컨테이너 안에서 실제로 만들어 실행하는 코드는 이런 모습이다. 루프로 여러 지역을 순회한다.
# Claude가 자동 생성하는 코드 예시
regions = ["서울", "부산", "대구", "인천", "광주"]
results = {}
for region in regions:
data = await query_database(
f"SELECT SUM(revenue) as total FROM sales WHERE region='{region}'"
)
results[region] = data[0]["total"]
# 최고 매출 지역 찾기
top_region = max(results.items(), key=lambda x: x[1])
print(f"최고 매출 지역: {top_region[0]} - {top_region[1]:,}원")
5번의 모델 왕복 대신 1번의 코드 실행으로 처리된다는 점이 핵심이다.
조건에 따라 다른 도구를 부르는 것도 컨테이너 안에서 자연스럽게 처리된다.
# Claude가 파일 크기에 따라 다른 도구 호출
file_info = await get_file_info(path)
if file_info["size"] < 10000:
content = await read_full_file(path) # 전체 읽기
else:
content = await read_file_summary(path) # 요약만 읽기
print(content)
대용량 데이터를 걸러낼 때도 전체를 모델에 돌려주는 대신 필요한 부분만 반환한다.
# 10만 건의 로그에서 에러만 추출
logs = await fetch_logs(server_id)
errors = [log for log in logs if "ERROR" in log]
# 마지막 10개 에러만 반환 (토큰 절약)
print(f"총 {len(errors)}개 에러 발견")
for error in errors[-10:]:
print(error)
조건을 만족하면 나머지를 확인하지 않고 바로 멈추는 패턴도 가능하다.
# 정상 엔드포인트를 찾으면 즉시 종료
endpoints = ["us-east", "eu-west", "apac"]
for endpoint in endpoints:
status = await check_health(endpoint)
if status == "healthy":
print(f"정상 엔드포인트 발견: {endpoint}")
break # 나머지는 확인하지 않음
응답 처리 워크플로우
전체 흐름은 초기 요청 → tool_use 응답 수신 → 도구 결과 제공 → (반복 또는 최종 응답)으로 이어진다.
┌──────────────────────────────────────────────────────────────────┐
│ Step 1: 초기 요청 │
│ client.beta.messages.create(...) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 2: API 응답 (stop_reason: "tool_use") │
│ - server_tool_use: 코드 실행 블록 │
│ - tool_use: 프로그래매틱 도구 호출 (caller 필드 포함) │
│ - container: {id, expires_at} │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 3: 도구 결과 제공 │
│ - tool_result 블록으로 응답 │
│ - container ID 재사용 │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ 추가 도구 호출? │
└──────────────────┘
│ │
Yes No
│ │
▼ ▼
Step 3 반복 최종 응답
초기 요청은 앞서 본 것과 같은 형태다.
response = client.beta.messages.create(
model="claude-sonnet-4-5",
betas=["advanced-tool-use-2025-11-20"],
max_tokens=4096,
messages=[{
"role": "user",
"content": "고객 구매 이력을 조회하고 상위 5명을 알려줘"
}],
tools=[
{"type": "code_execution_20250825", "name": "code_execution"},
{
"name": "query_database",
"description": "SQL 쿼리 실행. JSON 리스트 반환.",
"input_schema": {...},
"allowed_callers": ["code_execution_20250825"]
}
]
)
이 요청에 대한 응답에는 server_tool_use(코드 실행 블록)와 tool_use(caller 필드가 붙은 프로그래매틱 호출), 그리고 컨테이너 정보가 함께 담긴다.
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "구매 이력을 조회하고 분석하겠습니다."
},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "code_execution",
"input": {
"code": "results = await query_database('<sql>')\ntop_customers = sorted(results, key=lambda x: x['revenue'], reverse=True)[:5]\nprint(f'Top 5: {top_customers}')"
}
},
{
"type": "tool_use",
"id": "toolu_def456",
"name": "query_database",
"input": { "sql": "SELECT * FROM purchases" },
"caller": {
"type": "code_execution_20250825",
"tool_id": "srvtoolu_abc123"
}
}
],
"container": {
"id": "container_xyz789",
"expires_at": "2025-01-15T14:30:00Z"
},
"stop_reason": "tool_use"
}
도구 결과를 돌려줄 때는 이전 대화 기록을 그대로 유지하면서 container 필드에 같은 컨테이너 ID를 지정해 재사용한다.
response = client.beta.messages.create(
model="claude-sonnet-4-5",
betas=["advanced-tool-use-2025-11-20"],
max_tokens=4096,
container="container_xyz789", # 컨테이너 재사용!
messages=[
# 이전 대화 기록
{"role": "user", "content": "고객 구매 이력을 조회하고 상위 5명을 알려줘"},
{
"role": "assistant",
"content": [
{"type": "text", "text": "구매 이력을 조회하고 분석하겠습니다."},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "code_execution",
"input": {"code": "..."}
},
{
"type": "tool_use",
"id": "toolu_def456",
"name": "query_database",
"input": {"sql": "SELECT * FROM purchases"},
"caller": {
"type": "code_execution_20250825",
"tool_id": "srvtoolu_abc123"
}
}
]
},
# 도구 결과 제공
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_def456",
"content": '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}]'
}
]
}
],
tools=[...]
)
더 이상 도구 호출이 필요 없으면 stop_reason이 end_turn으로 바뀌고, 코드 실행 결과와 최종 텍스트가 함께 돌아온다.
{
"stop_reason": "end_turn",
"content": [
{
"type": "code_execution_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {
"type": "code_execution_result",
"stdout": "Top 5 고객:\n1. C1: 45,000원\n2. C2: 38,000원\n...",
"stderr": "",
"return_code": 0,
"content": []
}
},
{
"type": "text",
"text": "분석 결과, 상위 5명의 고객이 총 167,500원의 매출을 발생시켰습니다."
}
]
}
핵심 설정 옵션: allowed_callers와 caller
allowed_callers는 도구를 누가 부를 수 있는지를 정한다.
| 값 | 의미 | 사용 시점 |
|---|---|---|
["direct"] |
Claude가 직접 호출만 가능 (기본값) | 단순 도구, 즉각 응답 필요 |
["code_execution_20250825"] |
코드 실행 내에서만 호출 가능 | 배치 처리, 데이터 필터링 |
["direct", "code_execution_20250825"] |
둘 다 가능 | 유연성 필요 시 (권장하지 않음) |
도구별로 하나만 선택해야 Claude에게 명확한 지침이 전달된다.
응답의 caller 필드로 직접 호출인지 프로그래매틱 호출인지 구분한다. 직접 호출은 다음과 같은 형태다.
{
"type": "tool_use",
"id": "toolu_abc123",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": { "type": "direct" }
}
프로그래매틱 호출은 caller.type이 코드 실행 도구 버전으로 채워지고 tool_id가 함께 붙는다.
{
"type": "tool_use",
"id": "toolu_xyz789",
"name": "query_database",
"input": { "sql": "<sql>" },
"caller": {
"type": "code_execution_20250825",
"tool_id": "srvtoolu_abc123"
}
}
에러 처리
컨테이너 만료가 임박했는지는 expires_at을 직접 비교해서 확인한다.
from datetime import datetime, timedelta
def check_container_expiry(response):
if response.container and response.container.expires_at:
expiry = datetime.fromisoformat(
response.container.expires_at.replace('Z', '+00:00')
)
remaining = expiry - datetime.now(expiry.tzinfo)
if remaining < timedelta(minutes=1):
print(f"⚠️ 컨테이너 만료 임박: {remaining.seconds}초 남음")
return True
return False
도구 실행 중 에러가 나면 tool_result의 content에 에러 메시지를 그대로 담아 돌려준다.
# 도구 실행 중 에러 발생 시
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"content": "Error: 데이터베이스 연결 실패 - 30초 타임아웃"
}
자주 만나는 에러와 원인은 다음과 같다.
| 에러 | 원인 | 해결 방법 |
|---|---|---|
invalid_tool_input |
도구 입력이 스키마와 불일치 | input_schema 검증 |
tool_not_allowed |
allowed_callers 설정 오류 | 올바른 caller 유형 확인 |
missing_beta_header |
베타 헤더 누락 | betas=["advanced-tool-use-2025-11-20"] 추가 |
TimeoutError |
컨테이너 만료 | expires_at 모니터링, 빠른 응답 |
비용과 성능: 언제 쓸 가치가 있나
토큰 절약 효과는 시나리오마다 다르게 보고된다.
| 시나리오 | 직접 호출 | 프로그래매틱 | 절약률 |
|---|---|---|---|
| 5개 지역 매출 조회 | 5회 왕복 | 1회 코드 실행 | ~80% |
| 100개 로그 필터링 | 전체 반환 | 요약만 반환 | ~95% |
| 조건부 10회 호출 | 10회 왕복 | 1회 코드 실행 | ~90% |
대규모 데이터 집계·필터링, 3개 이상의 연속 도구 호출, 50개 엔드포인트 일괄 확인 같은 병렬 작업, 중간 결과가 최종 응답에 필요 없는 경우라면 프로그래매틱 방식이 맞다. 반대로 단순한 1회 도구 호출, 즉각적인 사용자 피드백이 필요한 상황, 컨테이너 오버헤드가 이득보다 큰 매우 빠른 작업에는 맞지 않는다.
모범 사례
도구 설계 단계에서는 반환 형식과 필드를 description에 명확히 적어둬야 Claude가 코드를 정확히 짠다.
# 좋은 예: 출력 형식 명확히 설명
{
"name": "query_database",
"description": """
SQL 쿼리를 실행합니다.
반환 형식: JSON 객체 배열
예시: [{"id": 1, "name": "홍길동", "revenue": 45000}]
필드:
- id: 정수, 고객 ID
- name: 문자열, 고객명
- revenue: 정수, 매출액 (원)
""",
"input_schema": {...},
"allowed_callers": ["code_execution_20250825"]
}
컨테이너는 전역 변수로 ID를 들고 있다가 다음 호출에 그대로 넘겨주는 식으로 재사용한다.
container_id = None
def call_with_container(messages):
global container_id
response = client.beta.messages.create(
model="claude-sonnet-4-5",
betas=["advanced-tool-use-2025-11-20"],
max_tokens=4096,
container=container_id, # 이전 컨테이너 재사용
messages=messages,
tools=[...]
)
# 컨테이너 ID 저장
if response.container:
container_id = response.container.id
return response
보안 측면에서는 외부 소스에서 반환된 데이터에 코드 주입 위험이 있는지 인지하고, SQL 인젝션 등을 막는 입력 검증을 하고, 민감 정보가 출력에 섞이지 않도록 제한해야 한다.
제약 사항
strict: true(구조화된 출력)는 지원하지 않고, tool_choice로 특정 도구를 강제할 수 없으며, disable_parallel_tool_use: true도 지원하지 않는다.
웹 검색, 웹 가져오기, MCP 커넥터 도구는 프로그래매틱 호출 대상이 아니다.
프로그래매틱 도구 호출에 대한 응답 메시지는 tool_result 블록만 담아야 한다. 다른 블록을 섞으면 에러가 난다.
# ❌ 잘못됨
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "toolu_01", "content": "..."},
{"type": "text", "text": "추가 질문입니다"} # 에러 발생!
]
}
# ✅ 올바름
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "toolu_01", "content": "..."}
]
}
도구와 데이터베이스: 흔히 하는 오해
도구(Tool)는 인터페이스 정의일 뿐이고, 실제 실행은 개발자의 몫이다.
┌─────────────────────────────────────────────────────────────────────────┐
│ 도구 호출의 실제 흐름 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 사용자 요청: "서울 매출 조회해줘" │
│ │ │
│ ▼ │
│ 2. Claude가 Python 코드 생성 (컨테이너 내) │
│ result = await query_database("SELECT * FROM sales") │
│ │ │
│ ▼ │
│ 3. API가 tool_use 반환 (stop_reason: "tool_use") │
│ { "name": "query_database", "input": {"sql": "SELECT..."} } │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 4. 🔴 개발자가 직접 구현해야 하는 부분! │ │
│ │ │ │
│ │ - 실제 DB 연결 (PostgreSQL, MySQL, SQLite 등) │ │
│ │ - SQL 쿼리 실행 │ │
│ │ - 결과를 JSON 문자열로 변환 │ │
│ │ - tool_result로 응답 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 5. Claude가 결과를 받아 처리 후 최종 응답 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
역할을 나눠보면 Claude는 도구 설명을 읽고 SQL을 생성하고 도구를 호출하는 데까지만 관여하고, 실제 DB 연결과 쿼리 실행은 전부 개발자 몫이다.
| 역할 | Claude가 하는 일 | 개발자가 하는 일 |
|---|---|---|
| 도구 정의 | 도구 설명을 읽고 이해 | 도구 스키마 작성 |
| SQL 생성 | 사용자 요청에 맞는 SQL 작성 | - |
| 도구 호출 | query_database(sql) 호출 |
- |
| DB 연결 | ❌ 불가능 | ✅ 직접 구현 |
| 쿼리 실행 | ❌ 불가능 | ✅ 직접 구현 |
| 결과 반환 | 받은 결과 처리 | JSON 문자열로 반환 |
DB 접속 정보는 Claude에게 절대 전달하지 않는다. 개발자 서버 코드에서 환경 변수로 관리한다.
# 개발자의 서버 코드에서 DB 연결 관리
import os
# 환경 변수로 관리 (보안)
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "mydb")
DB_USER = os.getenv("DB_USER", "admin")
DB_PASSWORD = os.getenv("DB_PASSWORD", "secret")
# PostgreSQL 예시
import psycopg2
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Claude가 호출한 도구를 실제로 실행"""
if tool_name == "query_database":
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)
cursor = conn.cursor()
cursor.execute(tool_input["sql"])
results = cursor.fetchall()
conn.close()
return json.dumps(results)
return json.dumps({"error": "Unknown tool"})
테이블 구조는 도구 description에 스키마를 명시해 Claude가 올바른 SQL을 생성하도록 유도한다.
{
"name": "query_database",
"description": """
SQL 쿼리를 실행합니다.
사용 가능한 테이블:
─────────────────────────────────────────
1. sales (매출 데이터)
- id: INTEGER, 기본 키
- region: TEXT, 지역명 (서울, 부산, 대구, 인천, 광주)
- product: TEXT, 제품명
- revenue: INTEGER, 매출액 (원)
- quarter: TEXT, 분기 (Q1, Q2, Q3, Q4)
2. customers (고객 데이터)
- id: INTEGER, 기본 키
- name: TEXT, 고객명
- email: TEXT, 이메일
- created_at: TIMESTAMP, 가입일
반환 형식: JSON 배열
예시: [{"region": "서울", "revenue": 45000000}]
""",
"input_schema": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SELECT 쿼리만 허용"}
},
"required": ["sql"]
},
"allowed_callers": ["code_execution_20250825"]
}
이 요소들을 전부 붙이면 다음과 같은 완전한 실행 예제가 된다. DB 설정, 도구 실행기, Claude 호출 루프 세 부분으로 나뉜다.
import anthropic
import json
import sqlite3
# =============================================================================
# 1단계: 실제 데이터베이스 설정 (개발자 영역)
# =============================================================================
def setup_database():
"""SQLite 인메모리 DB 생성 및 샘플 데이터 삽입"""
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 테이블 생성
cursor.execute("""
CREATE TABLE sales (
id INTEGER PRIMARY KEY,
region TEXT,
product TEXT,
revenue INTEGER,
quarter TEXT
)
""")
# 데이터 삽입
data = [
("서울", "노트북", 45000000, "Q4"),
("부산", "노트북", 28000000, "Q4"),
("대구", "노트북", 18000000, "Q4"),
]
cursor.executemany(
"INSERT INTO sales (region, product, revenue, quarter) VALUES (?, ?, ?, ?)",
data
)
conn.commit()
return conn
# =============================================================================
# 2단계: 도구 실행기 (개발자 영역)
# =============================================================================
class ToolExecutor:
def __init__(self, db_connection):
self.conn = db_connection
def execute(self, tool_name: str, tool_input: dict) -> str:
"""도구를 실행하고 결과를 JSON 문자열로 반환"""
if tool_name == "query_database":
sql = tool_input.get("sql", "")
# 보안: SELECT만 허용
if not sql.strip().upper().startswith("SELECT"):
return json.dumps({"error": "SELECT 쿼리만 허용됩니다"})
cursor = self.conn.cursor()
cursor.execute(sql)
rows = cursor.fetchall()
return json.dumps([dict(row) for row in rows], ensure_ascii=False)
return json.dumps({"error": f"알 수 없는 도구: {tool_name}"})
# =============================================================================
# 3단계: Claude API 호출 및 도구 처리 루프
# =============================================================================
def main():
# DB 설정
conn = setup_database()
executor = ToolExecutor(conn)
# Claude 클라이언트
client = anthropic.Anthropic()
# 도구 정의
tools = [
{"type": "code_execution_20250825", "name": "code_execution"},
{
"name": "query_database",
"description": """
sales 테이블을 조회합니다.
컬럼: id, region, product, revenue, quarter
반환: JSON 배열
""",
"input_schema": {
"type": "object",
"properties": {"sql": {"type": "string"}},
"required": ["sql"]
},
"allowed_callers": ["code_execution_20250825"]
}
]
messages = [{"role": "user", "content": "지역별 매출 합계를 알려줘"}]
container_id = None
# 도구 호출 루프
while True:
response = client.beta.messages.create(
model="claude-sonnet-4-5",
betas=["advanced-tool-use-2025-11-20"],
max_tokens=4096,
container=container_id,
messages=messages,
tools=tools
)
if response.container:
container_id = response.container.id
# 응답을 히스토리에 추가
messages.append({
"role": "assistant",
"content": [block.model_dump() for block in response.content]
})
# 종료 조건
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, 'text'):
print(f"최종 응답: {block.text}")
break
# 도구 호출 처리
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
# 🔴 여기서 실제 DB 쿼리 실행!
result = executor.execute(block.name, block.input)
print(f"도구 실행: {block.name} -> {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
conn.close()
if __name__ == "__main__":
main()
정리하면 DB는 개발자 서버·로컬에 있고 Claude는 접근할 수 없다. 접속 정보는 환경 변수로만 관리해 Claude에게 전달하지 않는다. 테이블 구조는 도구 description에 스키마로 명시하고, 실제 쿼리는 ToolExecutor 같은 개발자 코드에서 실행한다. Claude의 역할은 SQL 생성과 결과 분석·가공에 그친다.