1 언제 커스텀 도구가 필요한가
Claude Code와 GitHub Copilot은 파일 읽기·쓰기·웹 검색·셸 실행 같은 범용 도구를 내장하고 있다. 그러나 다음 상황에서는 내장 도구만으로 충분하지 않다.
| 상황 | 예시 | 내장 도구의 한계 |
|---|---|---|
| 사내 시스템 조회 | HR 시스템, 사내 JIRA, ERP | Bash로 curl 호출은 가능하나 인증·파싱이 매번 필요 |
| 도메인 특화 계산 | 금융 리스크 산출, 통계 검정 | Python 실행은 가능하나 반복 호출 시 코드 재작성 낭비 |
| 멀티스텝 복합 동작 | “PR 생성 + 리뷰어 할당 + Slack 알림” | 세 개의 API를 순서대로 호출하는 단일 인터페이스 없음 |
| 보안·규정 준수 | 마스킹된 PII 조회, 감사 로그 필기 | 데이터가 외부 서버를 경유하거나 로그가 남지 않음 |
| 성능 최적화 | 대용량 데이터 스트리밍, 캐시 조회 | 매 요청마다 전체 데이터를 읽어 토큰·시간 낭비 |
커스텀 도구는 이 빈틈을 표준 방식으로 메운다. 한 번 개발하면 Claude Code·Copilot 모두에서 네이티브 도구처럼 호출된다.
2 커스텀 도구 개발: MCP 서버
Claude Code와 GitHub Copilot 모두 MCP(Model Context Protocol) 표준을 사용하여 외부 도구를 탐색하고 호출한다. 따라서 커스텀 도구는 MCP 서버로 구현한다.
Agent가 MCP 서버에 연결하면 먼저 tools/list 요청을 보낸다. 서버는 등록된 모든 도구의 이름·설명·파라미터 스키마를 반환한다. Agent는 이 목록을 시스템 프롬프트에 주입한 뒤, 사용자 요청에 따라 적합한 도구를 선택하여 tools/call로 호출한다. 도구 설명(description)과 파라미터 스키마가 LLM이 읽는 유일한 도구 명세이므로, 이 텍스트의 품질이 호출 정확도를 결정한다.
2.1 기본 서버 구조
# custom_tools/server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
name="company-tools", # 서버 식별자 (Claude/Copilot에 표시됨)
instructions="""
회사 내부 시스템에 접근하는 도구 모음이다.
- HR 시스템 조회 (직원 정보, 조직도)
- JIRA 이슈 생성·조회·업데이트
- Slack 메시지 전송
인증이 필요한 도구는 환경 변수에서 토큰을 자동으로 읽는다.
"""
)instructions 필드는 서버 전체의 용도를 LLM에게 알려준다. Agent가 어떤 서버에서 도구를 찾아야 할지 판단하는 데 사용된다.
3 도구 명세 설계 원칙
커스텀 도구가 정확히 호출되려면 LLM이 이해할 수 있는 명세를 작성해야 한다. LLM은 코드 구현이 아니라 도구의 이름·설명·파라미터 스키마만 읽고 호출 여부를 판단한다.
3.1 원칙 1: 도구 이름 — 동사_목적어 형식
# Bad: 이름만으로 기능을 추측하기 어렵다
@mcp.tool()
def process(data: str) -> str: ...
@mcp.tool()
def handle_request(req: str) -> str: ...
# Good: 동사_목적어 형식으로 의도를 명확히 한다
@mcp.tool()
def get_employee_by_id(employee_id: str) -> str: ...
@mcp.tool()
def create_jira_issue(title: str, description: str, priority: str) -> str: ...
@mcp.tool()
def send_slack_message(channel: str, message: str) -> str: ...3.2 원칙 2: docstring — 3요소 구조
docstring은 FastMCP가 자동으로 도구 설명으로 변환한다. LLM이 “이 도구를 언제 써야 하는가”를 판단하는 근거가 된다.
@mcp.tool()
def get_employee_by_id(employee_id: str, include_org: bool = False) -> str:
"""사번으로 직원 정보를 조회한다.
HR 시스템에서 실시간으로 읽어오며, 이름·직급·부서·이메일을 반환한다.
조직도 포함 여부를 선택할 수 있다.
언제 사용하는가:
직원 정보가 필요할 때. 이름만 알고 있으면 search_employee_by_name을 사용한다.
Args:
employee_id: 6자리 사번 (예: '123456')
include_org: True이면 직속 상위 조직 3단계까지 포함한다 (기본값: False)
Returns:
JSON 문자열. 필드: id, name, title, department, email, manager_id
Example:
get_employee_by_id("123456") → '{"id": "123456", "name": "김광민", ...}'
"""
# 구현
...3요소: 무엇을 하는가 + 언제 쓰는가 + Args/Returns 명세. “언제 사용하는가” 항목은 유사 도구가 여러 개 있을 때 LLM이 올바른 도구를 선택하도록 돕는다.
3.3 원칙 3: 파라미터 스키마 — enum·범위·예시 포함
@mcp.tool()
def create_jira_issue(
project_key: str,
title: str,
description: str,
issue_type: str = "Task",
priority: str = "Medium",
assignee_id: str = ""
) -> str:
"""JIRA에 새 이슈를 생성하고 이슈 키를 반환한다.
Args:
project_key: JIRA 프로젝트 키 (예: 'BACKEND', 'DATA', 'INFRA')
title: 이슈 제목 (최대 255자)
description: 이슈 상세 설명. 마크다운 형식을 지원한다.
issue_type: 이슈 유형. 'Task', 'Bug', 'Story', 'Epic' 중 하나 (기본: 'Task')
priority: 우선순위. 'Highest', 'High', 'Medium', 'Low', 'Lowest' 중 하나 (기본: 'Medium')
assignee_id: 담당자 사번. 비워두면 미배정 상태로 생성된다.
Returns:
생성된 이슈 키 (예: 'BACKEND-1234')
"""
...허용 값(enum)을 파라미터 설명에 명시하면 LLM이 잘못된 값을 전달하는 오류를 줄일 수 있다.
3.4 원칙 4: 유사 도구 간 경계 명확화
기능이 겹치는 도구가 여러 개 있을 때 LLM이 혼동하지 않도록 상호 참조를 넣는다.
@mcp.tool()
def get_employee_by_id(employee_id: str) -> str:
"""사번으로 직원 정보를 조회한다.
사번을 이미 알고 있을 때 사용한다.
이름으로 검색하려면 search_employee_by_name을 사용한다.
"""
...
@mcp.tool()
def search_employee_by_name(name: str, department: str = "") -> str:
"""이름으로 직원을 검색하고 후보 목록을 반환한다.
이름은 알지만 사번을 모를 때 사용한다.
사번을 알고 있으면 get_employee_by_id가 더 정확하다.
동명이인이 있을 수 있으므로 결과를 확인 후 사번을 특정한다.
"""
...4 Claude Code에 커스텀 도구 등록
4.1 로컬 서버 등록 (stdio)
// ~/.claude.json (전역 설정 — 모든 프로젝트에 적용)
{
"mcpServers": {
"company-tools": {
"command": "python",
"args": ["-m", "custom_tools.server"],
"cwd": "/path/to/my/tools",
"env": {
"HR_API_TOKEN": "${HR_API_TOKEN}",
"JIRA_TOKEN": "${JIRA_TOKEN}",
"SLACK_TOKEN": "${SLACK_TOKEN}"
}
}
}
}// 프로젝트 루트/.claude.json (프로젝트 한정 적용)
{
"mcpServers": {
"company-tools": {
"command": "python",
"args": ["-m", "custom_tools.server"],
"cwd": "${workspaceFolder}"
}
}
}등록 후 Claude Code를 재시작하면 /mcp 명령으로 연결 상태와 도구 목록을 확인할 수 있다.
> /mcp
● company-tools (connected)
Tools: get_employee_by_id, search_employee_by_name, create_jira_issue,
send_slack_message, get_jira_issue, update_jira_status
4.2 원격 서버 등록 (HTTP/SSE)
팀 전체가 공유하는 도구는 원격 서버로 배포하고 URL로 등록한다.
# 원격 서버 실행 (HTTP SSE 모드)
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8080)// ~/.claude.json
{
"mcpServers": {
"company-tools": {
"url": "https://tools.mycompany.internal:8080/sse",
"headers": {
"Authorization": "Bearer ${INTERNAL_TOOLS_TOKEN}"
}
}
}
}5 GitHub Copilot에 커스텀 도구 등록
5.1 VS Code settings.json 등록
// .vscode/settings.json (프로젝트 공유 설정)
{
"github.copilot.chat.mcp.enabled": true,
"mcp": {
"servers": {
"company-tools": {
"command": "python",
"args": ["-m", "custom_tools.server"],
"cwd": "${workspaceFolder}",
"env": {
"HR_API_TOKEN": "${env:HR_API_TOKEN}",
"JIRA_TOKEN": "${env:JIRA_TOKEN}"
}
}
}
}
}등록 후 VS Code를 재로드하면 Copilot Chat 입력창 옆 도구 아이콘에서 company-tools의 도구 목록을 확인할 수 있다.
5.2 Copilot Agent Mode에서의 자동 탐색
Copilot Agent Mode(VS Code 1.99+)가 활성화된 상태에서 MCP 서버를 등록하면, Copilot이 사용자 요청에 따라 커스텀 도구를 자동으로 탐색하고 호출한다.
사용자: 123456 직원의 JIRA 이슈를 하나 만들어줘, 버그 리포트야
Copilot Agent:
1. get_employee_by_id("123456") 호출 → 직원 정보 확인
2. create_jira_issue(
project_key="BACKEND",
title="...",
issue_type="Bug",
assignee_id="123456"
) 호출
3. 결과를 사용자에게 보고
6 도구가 올바르게 호출되는지 검증
커스텀 도구를 등록한 후 LLM이 예상대로 선택하고 호출하는지 검증해야 한다.
6.1 MCP Inspector로 도구 테스트
브라우저에서 http://localhost:5173을 열면 도구 목록·파라미터 스키마를 확인하고, 직접 호출 테스트를 할 수 있다.
6.2 도구 호출 로그 확인
# server.py에 로깅 추가
import logging
logging.basicConfig(level=logging.DEBUG)
@mcp.tool()
def get_employee_by_id(employee_id: str) -> str:
"""사번으로 직원 정보를 조회한다. ..."""
logging.info(f"get_employee_by_id 호출: employee_id={employee_id}")
result = hr_api.get(employee_id)
logging.info(f"결과: {result}")
return resultClaude Code에서 MCP 통신 로그를 보려면:
6.3 도구 선택 실패 시 진단
LLM이 커스텀 도구 대신 기본 도구(Bash, WebSearch 등)를 선택한다면 다음을 점검한다.
| 증상 | 원인 | 해결 |
|---|---|---|
| 도구가 목록에 없음 | 서버 미등록 또는 연결 실패 | /mcp 명령으로 연결 상태 확인 |
| Bash로 대신 처리 | 도구 설명이 모호하거나 너무 짧음 | docstring에 “언제 사용하는가” 명시 |
| 잘못된 파라미터 전달 | 파라미터 설명이 불충분 | 허용 값·형식·예시를 Args에 추가 |
| 유사 도구 중 오선택 | 이름·설명이 겹침 | 도구 간 상호 참조로 경계 명확화 |
| 도구 호출 거부 | 보안 정책 또는 권한 모드 제한 | 권한 모드 확인, 사용자 승인 필요 여부 점검 |
7 실전 예시: 사내 데이터 파이프라인 도구 추가
기존 도구(Bash, WebSearch)가 커버하지 못하는 사내 데이터 플랫폼에 접근하는 도구를 추가하는 예시다.
# custom_tools/data_platform.py
from mcp.server.fastmcp import FastMCP
import os, requests
mcp = FastMCP(
name="data-platform",
instructions="""
사내 데이터 플랫폼 도구 모음. Airflow DAG 관리, Hive 쿼리 실행,
데이터 카탈로그 검색, 파이프라인 상태 조회를 지원한다.
인터넷 검색이나 공용 API가 아닌 사내 시스템 접근에만 사용한다.
"""
)
DATA_PLATFORM_URL = os.environ["DATA_PLATFORM_URL"]
API_TOKEN = os.environ["DATA_PLATFORM_TOKEN"]
@mcp.tool()
def list_dag_runs(dag_id: str, limit: int = 10) -> str:
"""특정 Airflow DAG의 최근 실행 이력을 조회한다.
DAG의 성공·실패·실행 중 상태와 실행 시간을 반환한다.
DAG ID를 모르면 search_dag를 먼저 사용한다.
Args:
dag_id: Airflow DAG ID (예: 'daily_user_feature_pipeline')
limit: 반환할 실행 이력 수. 최대 50개 (기본: 10)
Returns:
JSON 배열. 각 항목: run_id, state, start_date, end_date, duration_sec
"""
resp = requests.get(
f"{DATA_PLATFORM_URL}/api/v1/dags/{dag_id}/dagRuns",
headers={"Authorization": f"Bearer {API_TOKEN}"},
params={"limit": limit}
)
resp.raise_for_status()
return resp.text
@mcp.tool()
def run_hive_query(query: str, database: str = "default", timeout_sec: int = 60) -> str:
"""Hive에서 SQL 쿼리를 실행하고 결과를 반환한다.
분석용 읽기 전용 쿼리에 사용한다. SELECT만 허용되며
INSERT·UPDATE·DELETE·DROP은 실행이 거부된다.
Args:
query: 실행할 HiveQL 쿼리 (SELECT만 허용)
database: 사용할 Hive 데이터베이스 (기본: 'default')
timeout_sec: 쿼리 타임아웃 (초). 최대 300초 (기본: 60)
Returns:
CSV 형식의 쿼리 결과. 최대 1000행까지 반환된다.
Example:
run_hive_query("SELECT user_id, COUNT(*) FROM events GROUP BY user_id LIMIT 10")
"""
# SELECT만 허용하는 보안 검사
stripped = query.strip().upper()
if not stripped.startswith("SELECT"):
return "오류: SELECT 쿼리만 허용됩니다."
resp = requests.post(
f"{DATA_PLATFORM_URL}/api/v1/query",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={"query": query, "database": database, "timeout": timeout_sec}
)
resp.raise_for_status()
return resp.text
@mcp.tool()
def search_catalog(keyword: str, asset_type: str = "all") -> str:
"""데이터 카탈로그에서 테이블·컬럼·대시보드를 검색한다.
특정 데이터가 어느 테이블에 있는지 모를 때 먼저 사용한다.
테이블 이름을 알면 run_hive_query로 직접 조회한다.
Args:
keyword: 검색어 (테이블명, 컬럼명, 설명 텍스트 등)
asset_type: 검색 대상. 'table', 'column', 'dashboard', 'all' 중 하나 (기본: 'all')
Returns:
JSON 배열. 각 항목: asset_type, name, description, owner, last_updated
"""
resp = requests.get(
f"{DATA_PLATFORM_URL}/api/v1/catalog/search",
headers={"Authorization": f"Bearer {API_TOKEN}"},
params={"q": keyword, "type": asset_type}
)
resp.raise_for_status()
return resp.text
if __name__ == "__main__":
mcp.run() # stdio 모드등록 후 Claude에서 자연어로 호출한다:
사용자: 어제 daily_user_feature_pipeline이 실패했어? 원인 파악해줘
Claude:
1. list_dag_runs("daily_user_feature_pipeline", limit=5) 호출
→ 어제 실행이 "failed" 상태임을 확인
2. 실패 로그 확인을 위해 추가 도구 호출 또는 Bash로 로그 조회
3. 원인 분석 및 보고
8 도구 유지보수 체크리스트
커스텀 도구를 팀에 배포할 때 점검할 사항이다. 개발 단계에서 빠트리기 쉬운 항목들이지만, 이 중 하나라도 누락되면 LLM이 도구를 잘못 선택하거나 민감 정보가 노출되거나 팀원이 도구를 사용하지 못하는 상황이 발생한다.
9 관련 주제
선행 지식
- MCP 기반 도구 통합 — MCP 아키텍처, 연결 방식 분류
- AI Coding Assistant의 SW Tool SDK 명세 — 내장 도구 전체 목록 (보강 대상 파악)
후속 주제
- 자체 개발 Agent에 Claude·Copilot 도구 연결하기 — 커스텀 도구를 Agent에 연결하는 오케스트레이션
- Agent 스킬 설계와 생성 — 도구 집합을 스킬로 구조화하는 방법
공식 문서