LLM이 서버를 통해 작업을 수행할 수 있게 하기
도구(Tools)는 Model Context Protocol(MCP)의 강력한 기본 요소로, 서버가 실행 가능한 기능을 클라이언트에 노출할 수 있게 합니다. 도구를 통해 LLM은 외부 시스템과 상호작용하고, 계산을 수행하며, 실제 세계에서 작업을 수행할 수 있습니다.
도구는 모델 제어(model-controlled)가 되도록 설계되었습니다. 이는 AI 모델이 자동으로 호출할 수 있도록(승인을 부여하는 사람이 포함된 상태에서) 서버에서 클라이언트로 도구가 노출된다는 의미입니다.
개요
MCP의 도구는 서버가 클라이언트에 의해 호출되고 LLM이 작업을 수행하는 데 사용할 수 있는 실행 가능한 함수를 노출할 수 있게 합니다. 도구의 주요 측면은 다음과 같습니다:
- 검색: 클라이언트는
tools/list
엔드포인트를 통해 사용 가능한 도구를 나열할 수 있습니다 - 호출: 도구는
tools/call
엔드포인트를 사용하여 호출되며, 서버는 요청된 작업을 수행하고 결과를 반환합니다 - 유연성: 도구는 간단한 계산부터 복잡한 API 상호작용까지 다양할 수 있습니다
리소스와 마찬가지로, 도구는 고유한 이름으로 식별되며 사용 방법을 안내하기 위한 설명을 포함할 수 있습니다. 그러나 리소스와 달리, 도구는 상태를 수정하거나 외부 시스템과 상호작용할 수 있는 동적 작업을 나타냅니다.
도구 정의 구조
각 도구는 다음 구조로 정의됩니다:
{
name: string; // 도구의 고유 식별자
description?: string; // 사람이 읽을 수 있는 설명
inputSchema: { // 도구 매개변수의 JSON 스키마
type: "object",
properties: { ... } // 도구별 매개변수
},
annotations?: { // 도구 동작에 대한 선택적 힌트
title?: string; // 도구의 사람이 읽을 수 있는 제목
readOnlyHint?: boolean; // true인 경우 도구가 환경을 수정하지 않음
destructiveHint?: boolean; // true인 경우 도구가 파괴적 업데이트를 수행할 수 있음
idempotentHint?: boolean; // true인 경우 동일한 인수로 반복 호출해도 추가 효과 없음
openWorldHint?: boolean; // true인 경우 도구가 외부 엔티티와 상호작용함
}
}
도구 구현하기
다음은 MCP 서버에서 기본 도구를 구현하는 예시입니다:
- TypeScript
- Python
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// 사용 가능한 도구 정의
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "두 숫자를 더합니다",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});
// 도구 실행 처리
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
content: [
{
type: "text",
text: String(a + b)
}
]
};
}
throw new Error("도구를 찾을 수 없습니다");
});
app = Server("example-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="calculate_sum",
description="두 숫자를 더합니다",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}
)
]
@app.call_tool()
async def call_tool(
name: str,
arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name == "calculate_sum":
a = arguments["a"]
b = arguments["b"]
result = a + b
return [types.TextContent(type="text", text=str(result))]
raise ValueError(f"도구를 찾을 수 없습니다: {name}")
도구 패턴 예시
서버가 제공할 수 있는 도구 유형의 예시는 다음과 같습니다:
시스템 작업
로컬 시스템과 상호작용하는 도구:
{
name: "execute_command",
description: "셸 명령어 실행",
inputSchema: {
type: "object",
properties: {
command: { type: "string" },
args: { type: "array", items: { type: "string" } }
}
}
}
API 통합
외부 API를 래핑하는 도구:
{
name: "github_create_issue",
description: "GitHub 이슈 생성",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string" },
labels: { type: "array", items: { type: "string" } }
}
}
}
데이터 처리
데이터를 변환하거나 분석하는 도구:
{
name: "analyze_csv",
description: "CSV 파일 분석",
inputSchema: {
type: "object",
properties: {
filepath: { type: "string" },
operations: {
type: "array",
items: {
enum: ["sum", "average", "count"]
}
}
}
}
}
모범 사례
도구를 구현할 때:
- 명확하고 설명적인 이름과 설명 제공
- 매개변수에 대한 상세한 JSON 스키마 정의 사용
- 모델이 어떻게 사용해야 하는지 보여주기 위해 도구 설명에 예시 포함
- 적절한 오류 처리 및 검증 구현
- 긴 작업에는 진행 상황 보고 사용
- 도구 작업을 집중적이고 원자적으로 유지
- 예상되는 반환 값 구조 문서화
- 적절한 타임아웃 구현
- 리소스 집약적 작업에 대한 속도 제한 고려
- 디버깅 및 모니터링을 위한 도구 사용 로깅
보안 고려사항
도구를 노출할 때:
입력 검증
- 모든 매개변수를 스키마에 대해 검증
- 파일 경로 및 시스템 명령어 정제
- URL 및 외부 식별자 검증
- 매개변수 크기 및 범위 확인
- 명령어 인젝션 방지
접근 제어
- 필요한 경우 인증 구현
- 적절한 권한 부여 확인 사용
- 도구 사용 감사
- 요청 속도 제한
- 남용 모니터링
오류 처리
- 내부 오류를 클라이언트에 노출하지 않음
- 보안 관련 오류 로깅
- 타임아웃 적절히 처리
- 오류 후 리소스 정리
- 반환 값 검증
도구 검색 및 업데이트
MCP는 동적 도구 검색을 지원합니다:
- 클라이언트는 언제든지 사용 가능한 도구를 나열할 수 있습니다
- 서버는
notifications/tools/list_changed
를 사용하여 도구가 변경될 때 클라이언트에 알릴 수 있습니다 - 런타임 중에 도구를 추가하거나 제거할 수 있습니다
- 도구 정의를 업데이트할 수 있습니다(단, 이는 신중하게 수행해야 함)
오류 처리
도구 오류는 MCP 프로토콜 수준 오류가 아닌 결과 객체 내에서 보고되어야 합니다. 이를 통해 LLM이 오류를 보고 잠재적으로 처리할 수 있습니다. 도구가 오류를 발생시킬 때:
- 결과에서
isError
를true
로 설정 content
배열에 오류 세부 정보 포함
다음은 도구에 대한 적절한 오류 처리 예시입니다:
- TypeScript
- Python
try {
// 도구 작업
const result = performOperation();
return {
content: [
{
type: "text",
text: `작업 성공: ${result}`
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `오류: ${error.message}`
}
]
};
}
try:
# 도구 작업
result = perform_operation()
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=f"작업 성공: {result}"
)
]
)
except Exception as error:
return types.CallToolResult(
isError=True,
content=[
types.TextContent(
type="text",
text=f"오류: {str(error)}"
)
]
)
이 접근 방식을 통해 LLM은 오류가 발생했음을 확인하고 잠재적으로 수정 조치를 취하거나 사용자 개입을 요청할 수 있습니다.
도구 주석
도구 주석은 도구의 동작에 대한 추가 메타데이터를 제공하여 클라이언트가 도구를 표시하고 관리하는 방법을 이해하는 데 도움을 줍니다. 이러한 주석은 도구의 특성과 영향을 설명하는 힌트이지만, 보안 결정을 위해 의존해서는 안 됩니다.
도구 주석의 목적
도구 주석은 다음과 같은 여러 가지 주요 목적을 제공합니다:
- 모델 컨텍스트에 영향을 주지 않고 UX 관련 정보 제공
- 클라이언트가 도구를 적절하게 분류하고 표시하는 데 도움
- 도구의 잠재적 부작용에 대한 정보 전달
- 도구 승인을 위한 직관적인 인터페이스 개발 지원
사용 가능한 도구 주석
MCP 사양은 도구에 대해 다음과 같은 주석을 정의합니다:
주석 | 타입 | 기본값 | 설명 |
---|---|---|---|
title | string | - | UI 표시에 유용한 도구의 사람이 읽을 수 있는 제목 |
readOnlyHint | boolean | false | true인 경우, 도구가 환경을 수정하지 않음을 나타냄 |
destructiveHint | boolean | true | true인 경우, 도구가 파괴적 업데이트를 수행할 수 있음(readOnlyHint가 false일 때만 의미 있음) |
idempotentHint | boolean | false | true인 경우, 동일한 인수로 도구를 반복적으로 호출해도 추가 효과가 없음(readOnlyHint가 false일 때만 의미 있음) |
openWorldHint | boolean | true | true인 경우, 도구가 외부 엔티티의 "오픈 월드"와 상호작용할 수 있음 |
사용 예시
다음은 다양한 시나리오에 대한 주석이 있는 도구를 정의하는 방법입니다:
// 읽기 전용 검색 도구
{
name: "web_search",
description: "정보를 위해 웹 검색",
inputSchema: {
type: "object",
properties: {
query: { type: "string" }
},
required: ["query"]
},
annotations: {
title: "웹 검색",
readOnlyHint: true,
openWorldHint: true
}
}
// 파괴적인 파일 삭제 도구
{
name: "delete_file",
description: "파일 시스템에서 파일 삭제",
inputSchema: {
type: "object",
properties: {
path: { type: "string" }
},
required: ["path"]
},
annotations: {
title: "파일 삭제",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false
}
}
// 비파괴적 데이터베이스 레코드 생성 도구
{
name: "create_record",
description: "데이터베이스에 새 레코드 생성",
inputSchema: {
type: "object",
properties: {
table: { type: "string" },
data: { type: "object" }
},
required: ["table", "data"]
},
annotations: {
title: "데이터베이스 레코드 생성",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false
}
}
서버 구현에 주석 통합하기
- TypeScript
- Python
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "두 숫자를 더합니다",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
},
annotations: {
title: "합계 계산",
readOnlyHint: true,
openWorldHint: false
}
}]
};
});
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("example-server")
@mcp.tool(
annotations={
"title": "합계 계산",
"readOnlyHint": True,
"openWorldHint": False
}
)
async def calculate_sum(a: float, b: float) -> str:
"""두 숫자를 더합니다.
Args:
a: 더할 첫 번째 숫자
b: 더할 두 번째 숫자
"""
result = a + b
return str(result)
도구 주석에 대한 모범 사례
- 부작용에 대해 정확하게 표시: 도구가 환경을 수정하는지 여부와 수정이 파괴적인지 여부를 명확하게 표시합니다.
- 설명적인 제목 사용: 도구의 목적을 명확하게 설명하는 사람 친화적인 제목을 제공합니다.
- 멱등성을 적절하게 표시: 동일한 인수로 반복 호출해도 추가 효과가 없는 경우에만 도구를 멱등적으로 표시합니다.
- 적절한 개방/폐쇄 세계 힌트 설정: 도구가 폐쇄 시스템(데이터베이스와 같은) 또는 개방 시스템(웹과 같은)과 상호작용하는지 표시합니다.
- 주석은 힌트임을 기억하세요: ToolAnnotations의 모든 속성은 힌트이며 도구 동작에 대한 충실한 설명을 제공한다고 보장할 수 없습니다. 클라이언트는 주석만을 기반으로 보안 중요 결정을 내려서는 안 됩니다.
도구 테스트
MCP 도구에 대한 종합적인 테스트 전략은 다음을 포함해야 합니다:
- 기능 테스트: 도구가 유효한 입력으로 올바르게 실행되고 유효하지 않은 입력을 적절히 처리하는지 확인
- 통합 테스트: 실제 및 모의 종속성을 모두 사용하여 외부 시스템과의 도구 상호작용 테스트
- 보안 테스트: 인증, 권한 부여, 입력 정제 및 속도 제한 검증
- 성능 테스트: 부하 하에서의 동작, 타임아웃 처리 및 리소스 정리 확인
- 오류 처리: 도구가 MCP 프로토콜을 통해 오류를 적절히 보고하고 리소스를 정리하는지 확인