Claude·Copilot 도구 생태계에 커스텀 도구 추가하기

기존 도구의 빈틈을 메우는 도구 개발·등록·호출 최적화 실전 가이드

Claude Code·GitHub Copilot의 내장 도구가 커버하지 못하는 상황을 진단하고, 커스텀 도구를 직접 개발하여 기존 도구 목록에 추가하는 전 과정을 다룬다. MCP 서버 구현, 도구 발견(discovery) 메커니즘, LLM이 도구를 정확히 호출하게 만드는 명세 설계 원칙, 디버깅 방법까지 실전 구현 중심으로 정리한다.

Agent
Architecture
MCP
Tool
저자

Kwangmin Kim

공개

2026년 03월 29일

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 서버로 구현한다.

MCP 도구 발견(Discovery) 메커니즘

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로 도구 테스트

# MCP 공식 디버깅 도구
npx @modelcontextprotocol/inspector python -m custom_tools.server

브라우저에서 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 result

Claude Code에서 MCP 통신 로그를 보려면:

# Claude Code 디버그 모드로 실행
claude --mcp-debug

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 관련 주제

선행 지식

후속 주제

공식 문서

Subscribe

Enjoy this blog? Get notified of new posts by email: