MCP 서버가 제공하는 세 가지 핵심 프리미티브의 스키마 정의, 구현 패턴, 실전 활용 사례를 상세히 다룹니다.
MCP 서버는 클라이언트에 기능을 노출하기 위해 세 가지 프리미티브(Primitive)를 사용합니다. 각 프리미티브는 제어 주체와 용도가 명확히 구분되어 있습니다.
| 프리미티브 | 제어 주체 | 용도 | 비유 |
|---|---|---|---|
| 도구(Tools) | LLM이 결정 | 작업 수행 | API 엔드포인트 |
| 리소스(Resources) | 애플리케이션이 선택 | 데이터 제공 | 파일 시스템 |
| 프롬프트(Prompts) | 사용자가 선택 | 워크플로우 템플릿 | 명령 팔레트 |
이 구분은 단순한 분류가 아니라 설계 원칙입니다. 도구는 부작용(side effect)이 있을 수 있지만, 리소스는 읽기 전용으로 부작용이 없어야 합니다. 프롬프트는 여러 도구와 리소스를 조합한 상위 수준의 워크플로우를 정의합니다.
도구는 MCP 서버가 제공하는 실행 가능한 함수입니다. LLM이 사용자의 요청을 분석하여 적절한 도구를 선택하고, 필요한 매개변수를 구성하여 호출합니다.
도구는 JSON Schema로 입력 매개변수를 정의합니다. 이 스키마는 LLM이 올바른 매개변수를 구성하는 데 사용되며, 서버 측에서 입력 유효성 검사에도 활용됩니다.
{
"name": "create_issue",
"description": "GitHub 리포지토리에 새 이슈를 생성합니다. 버그 리포트, 기능 요청, 질문 등을 이슈로 등록할 때 사용합니다.",
"inputSchema": {
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "리포지토리 소유자의 GitHub 사용자명"
},
"repo": {
"type": "string",
"description": "리포지토리 이름"
},
"title": {
"type": "string",
"description": "이슈 제목"
},
"body": {
"type": "string",
"description": "이슈 본문 (Markdown 형식)"
},
"labels": {
"type": "array",
"items": { "type": "string" },
"description": "이슈에 붙일 라벨 목록"
}
},
"required": ["owner", "repo", "title"]
}
}도구의 description은 LLM이 도구를 선택할 때 참고하는 핵심 정보입니다. 어떤 상황에서 이 도구를 사용해야 하는지 명확히 기술해야 합니다. 단순히 "이슈를 생성합니다"보다 "GitHub 리포지토리에 새 이슈를 생성합니다. 버그 리포트, 기능 요청, 질문 등을 이슈로 등록할 때 사용합니다"처럼 구체적으로 작성하는 것이 효과적입니다.
클라이언트가 tools/call 메서드로 도구를 호출하면, 서버는 작업을 수행하고 결과를 반환합니다.
server.tool(
"analyze_code",
"코드를 분석하여 개선 사항을 제안합니다",
{
code: { type: "string", description: "분석할 코드" },
language: { type: "string", description: "프로그래밍 언어" },
},
async (params) => {
const analysis = await performAnalysis(params.code, params.language);
return {
content: [
{
type: "text",
text: analysis.summary,
},
{
type: "text",
text: "상세 분석 결과:\n" + analysis.details,
},
],
};
}
);도구 결과는 content 배열로 반환되며, 각 항목은 텍스트, 이미지, 또는 임베디드 리소스일 수 있습니다.
// 텍스트 콘텐츠
{ type: "text", text: "분석 결과입니다" }
// 이미지 콘텐츠
{ type: "image", data: "base64EncodedData...", mimeType: "image/png" }
// 임베디드 리소스
{
type: "resource",
resource: {
uri: "file:///path/to/result.json",
text: '{"key": "value"}',
mimeType: "application/json"
}
}실전에서 효과적인 도구를 설계하기 위한 원칙을 정리합니다.
원자적 작업 단위로 설계합니다. 하나의 도구는 하나의 작업만 수행해야 합니다. "이슈를 생성하고 담당자를 배정하는" 도구보다 "이슈를 생성하는" 도구와 "담당자를 배정하는" 도구를 분리하는 것이 좋습니다. LLM이 필요에 따라 조합하여 사용할 수 있기 때문입니다.
매개변수 설명을 구체적으로 작성합니다. LLM은 매개변수의 description을 보고 어떤 값을 전달해야 하는지 판단합니다. "쿼리"보다 "SQL SELECT 쿼리. WHERE 절을 포함할 수 있으며, 테이블 이름은 스키마에서 확인하세요"처럼 상세하게 기술합니다.
에러 상황을 명시적으로 처리합니다. 도구 실행이 실패할 경우, isError: true와 함께 사용자가 이해할 수 있는 에러 메시지를 반환합니다.
server.tool("delete_file", "파일을 삭제합니다", {
path: { type: "string", description: "삭제할 파일의 절대 경로" },
}, async (params) => {
try {
const fs = await import("fs/promises");
await fs.unlink(params.path);
return {
content: [{ type: "text", text: params.path + " 파일이 삭제되었습니다." }],
};
} catch (error) {
return {
content: [{
type: "text",
text: "파일 삭제에 실패했습니다: " + (error as Error).message,
}],
isError: true,
};
}
});2025년 6월 사양부터 도구에 주석(Annotations)을 추가할 수 있습니다. 주석은 도구의 동작 특성을 메타데이터로 제공하여, 클라이언트가 도구를 적절히 표시하거나 제어하는 데 활용됩니다.
server.tool(
"send_email",
"이메일을 발송합니다",
{ to: { type: "string" }, subject: { type: "string" }, body: { type: "string" } },
async (params) => {
// 이메일 발송 로직
return { content: [{ type: "text", text: "이메일이 발송되었습니다." }] };
},
{
annotations: {
destructive: true, // 되돌릴 수 없는 작업
readOnlyHint: false, // 읽기 전용이 아님
idempotent: false, // 동일 입력으로 재실행 시 다른 결과 가능
openWorld: true, // 외부 시스템과 상호작용
}
}
);| 주석 | 설명 | 기본값 |
|---|---|---|
destructive | 되돌릴 수 없는 변경을 수행하는지 | true |
readOnlyHint | 읽기 전용 작업인지 | false |
idempotent | 동일 입력에 동일 결과를 보장하는지 | false |
openWorld | 외부 세계와 상호작용하는지 | true |
클라이언트는 이 주석을 활용하여, 예를 들어 destructive: true인 도구를 호출하기 전에 사용자에게 확인을 요청할 수 있습니다.
리소스는 MCP 서버가 제공하는 읽기 전용 데이터입니다. 각 리소스는 고유한 URI로 식별되며, 텍스트 또는 바이너리 데이터를 포함합니다.
도구와의 핵심적인 차이점은 다음과 같습니다.
리소스는 두 가지 방식으로 노출됩니다.
직접 리소스(Direct Resources): 고정된 URI로 접근하는 구체적인 데이터입니다.
server.resource(
"project-config",
"file:///project/config.json",
{ description: "프로젝트 설정 파일", mimeType: "application/json" },
async () => ({
contents: [{
uri: "file:///project/config.json",
text: JSON.stringify(projectConfig, null, 2),
mimeType: "application/json",
}],
})
);리소스 템플릿(Resource Templates): URI 패턴을 사용하여 동적 리소스를 정의합니다. 클라이언트가 패턴에 맞는 URI를 구성하여 요청합니다.
server.resourceTemplate(
"db-record",
"db:///{table}/{id}",
{ description: "데이터베이스 레코드", mimeType: "application/json" },
async (uri, params) => {
const record = await db.query(
"SELECT * FROM " + params.table + " WHERE id = " + params.id
);
return {
contents: [{
uri: uri.toString(),
text: JSON.stringify(record),
mimeType: "application/json",
}],
};
}
);클라이언트는 리소스의 변경 사항을 구독할 수 있습니다. 서버가 resources 능력에 subscribe: true를 선언한 경우에만 사용 가능합니다.
이 메커니즘은 설정 파일이 변경되거나, 데이터베이스 레코드가 업데이트되는 등의 상황에서 유용합니다.
실전에서 리소스가 효과적으로 활용되는 패턴을 소개합니다.
프로젝트 컨텍스트 제공: 프로젝트의 구조, 설정, 규칙 등을 리소스로 노출하여 LLM이 프로젝트를 이해하는 데 필요한 컨텍스트를 제공합니다.
server.resource(
"project-structure",
"project://structure",
{ description: "프로젝트 디렉토리 구조" },
async () => ({
contents: [{
uri: "project://structure",
text: await generateDirectoryTree("./src"),
}],
})
);
server.resource(
"coding-standards",
"project://standards",
{ description: "팀 코딩 표준 및 규칙" },
async () => ({
contents: [{
uri: "project://standards",
text: await readFile("./.cursor/rules.md"),
}],
})
);데이터 탐색: 데이터베이스 스키마, 테이블 목록, 최근 로그 등을 리소스로 노출합니다.
server.resource(
"db-schema",
"db://schema",
{ description: "데이터베이스 전체 스키마" },
async () => ({
contents: [{
uri: "db://schema",
text: await getSchemaDefinition(),
mimeType: "application/json",
}],
})
);프롬프트는 MCP 서버가 제공하는 재사용 가능한 워크플로우 템플릿입니다. 단순한 문자열 템플릿이 아니라, 동적으로 생성되는 구조화된 메시지 시퀀스입니다.
프롬프트의 핵심 가치는 전문 지식의 캡슐화에 있습니다. 예를 들어 "코드 리뷰" 프롬프트는 코드 리뷰에 필요한 관점(보안, 성능, 가독성 등)을 미리 정의하고, 리뷰 대상 코드를 매개변수로 받아 완성된 리뷰 요청을 생성합니다.
server.prompt(
"code-review",
"코드 리뷰를 수행합니다",
[
{ name: "code", description: "리뷰할 코드", required: true },
{ name: "language", description: "프로그래밍 언어", required: false },
{ name: "focus", description: "집중할 영역 (security/performance/readability)", required: false },
],
async (params) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: [
"다음 " + (params.language || "") + " 코드를 리뷰해 주세요.",
params.focus ? "특히 " + params.focus + "에 집중해 주세요." : "",
"",
"리뷰 관점:",
"1. 버그 가능성",
"2. 보안 취약점",
"3. 성능 개선 여지",
"4. 코드 가독성",
"5. 모범 사례 준수 여부",
"",
"코드:",
"```" + (params.language || ""),
params.code,
"```",
].filter(Boolean).join("\n"),
},
},
],
})
);클라이언트는 prompts/list로 사용 가능한 프롬프트를 조회하고, prompts/get으로 특정 프롬프트를 매개변수와 함께 요청합니다.
프롬프트는 임베디드 리소스를 포함할 수 있습니다. 이를 통해 관련 데이터를 자동으로 컨텍스트에 포함시킬 수 있습니다.
server.prompt(
"debug-error",
"에러를 분석하고 수정 방안을 제시합니다",
[
{ name: "error_message", description: "에러 메시지", required: true },
],
async (params) => {
const recentLogs = await getRecentLogs();
const config = await getAppConfig();
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "다음 에러를 분석하고 수정 방안을 제시해 주세요.\n\n에러: " + params.error_message,
},
},
{
role: "user",
content: {
type: "resource",
resource: {
uri: "app://logs/recent",
text: recentLogs,
mimeType: "text/plain",
},
},
},
{
role: "user",
content: {
type: "resource",
resource: {
uri: "app://config",
text: JSON.stringify(config, null, 2),
mimeType: "application/json",
},
},
},
],
};
}
);이 프롬프트를 실행하면, 에러 메시지와 함께 최근 로그와 애플리케이션 설정이 자동으로 컨텍스트에 포함됩니다. 사용자가 일일이 관련 정보를 수집하여 붙여넣을 필요가 없습니다.
서버 프리미티브 외에 클라이언트 프리미티브도 존재합니다. 이들은 서버가 클라이언트에 요청하는 역방향 기능입니다.
루트는 클라이언트가 서버에 알려주는 작업 경계입니다. 서버가 접근할 수 있는 파일 시스템 경로나 URI 범위를 정의합니다.
{
"roots": [
{ "uri": "file:///home/user/project", "name": "메인 프로젝트" },
{ "uri": "file:///home/user/config", "name": "설정 디렉토리" }
]
}루트를 설정하면 서버는 지정된 경계 내에서만 작업을 수행합니다. 이는 보안 측면에서 중요하며, 서버가 의도하지 않은 파일에 접근하는 것을 방지합니다.
샘플링은 서버가 클라이언트에 LLM 호출을 요청하는 기능입니다. 서버가 직접 LLM API를 호출하지 않고, 클라이언트의 LLM을 활용할 수 있습니다.
이 기능은 서버가 복잡한 작업을 수행하면서 중간 단계에서 AI 추론이 필요한 경우에 유용합니다. 예를 들어 코드 분석 서버가 분석 결과를 자연어로 요약하기 위해 샘플링을 요청할 수 있습니다.
유도는 서버가 클라이언트를 통해 사용자에게 추가 정보를 요청하는 기능입니다. 2025년 6월 사양에서 추가되었으며, 두 가지 모드를 지원합니다.
폼 모드: JSON Schema를 사용하여 구조화된 입력을 요청합니다.
{
"method": "elicitation/create",
"params": {
"message": "데이터베이스 연결 정보를 입력해 주세요.",
"requestedSchema": {
"type": "object",
"properties": {
"host": { "type": "string", "description": "호스트 주소" },
"port": { "type": "number", "description": "포트 번호" },
"database": { "type": "string", "description": "데이터베이스 이름" }
},
"required": ["host", "database"]
}
}
}URL 모드: 사용자를 외부 URL로 안내합니다. OAuth 인증 등 민감한 정보를 MCP 클라이언트를 거치지 않고 직접 처리해야 하는 경우에 사용합니다.
유도 기능은 비교적 최근에 추가된 기능으로, 모든 클라이언트가 지원하지 않을 수 있습니다. 서버는 클라이언트의 능력 협상에서 elicitation 능력이 선언되었는지 확인한 후에만 유도를 사용해야 합니다.
실전에서는 세 가지 프리미티브를 조합하여 완성도 높은 MCP 서버를 구축합니다. 다음은 GitHub 통합 서버의 프리미티브 구성 예시입니다.
이 서버에서 사용자가 "코드 리뷰" 프롬프트를 실행하면, 프롬프트가 리소스에서 코드와 PR 정보를 가져오고, LLM이 리뷰를 수행한 후, 도구를 사용하여 리뷰 코멘트를 실제로 작성합니다. 프리미티브들이 유기적으로 연결되어 완전한 워크플로우를 구성하는 것입니다.
이 장에서 다룬 핵심 내용을 요약합니다.
5장에서는 TypeScript SDK를 사용하여 실제 MCP 서버를 구축합니다. 프로젝트 설정부터 도구, 리소스, 프롬프트 구현, 테스트, 배포까지 전체 과정을 단계별로 진행하겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
TypeScript SDK를 사용하여 프로젝트 설정부터 도구, 리소스, 프롬프트 구현, 테스트까지 MCP 서버를 구축하는 전 과정을 다룹니다.
MCP의 두 가지 핵심 전송 방식인 stdio와 Streamable HTTP의 동작 원리, 장단점, 선택 기준을 상세히 다룹니다.
Python의 FastMCP 프레임워크를 사용하여 데코레이터 기반의 간결하고 직관적인 MCP 서버를 구축하는 방법을 다룹니다.