MCP 서버를 처음부터 구축하고 테스트하기
요약
Model Context Protocol(MCP)을 사용하여 HTTP API를 에이전트용 도구로 변환하는 서버 구축 과정을 다룬 중급 튜토리얼입니다. TypeScript와 Node.js를 활용해 프로토콜 경계를 설정하고, 에이전트 프레임워크와 호환되는 서버를 구현 및 테스트하는 방법을 설명합니다.
핵심 포인트
- MCP를 통한 도구 통합의 표준화 및 재사용성 확보
- HTTP API를 MCP 서버로 래핑하여 에이전트에 노출하는 방법
- TypeScript와 Node.js 기반의 서버 구현 및 테스트 절차
- 에이전트 루프와 MCP 호환 호스트 간의 연결 검증
Agent Lab Journal
Guides
...
중급 튜토리얼
MCP 서버를 처음부터 구축하고 테스트하기
Схема составлена по утверждённому брифу и source_url будущей статьи
도구 통합 (Tool integrations)은 매번 새로운 에이전트 프레임워크 (agent framework)를 위해 다시 작성될 필요가 없어야 합니다. 이 가이드에서는 일반적인 HTTP API 앞에 안정적인 프로토콜 경계 (protocol boundary)를 배치하고, 하나의 유용한 도구 (tool)를 노출하며, 모델을 개입시키지 않고 이를 테스트한 다음, 에이전트 (agent)에 연결하는 과정을 다룹니다.
45분 읽기
중급
2026년 8월 2일 업데이트
목차
-
무엇을 구축하게 될 것인가
-
아키텍처 및 프로토콜 경계 (Architecture and protocol boundary)
-
프로젝트 준비
-
HTTP API 클라이언트 구축
-
MCP 서버 구현
-
클라이언트로 서버 테스트
-
LLM 에이전트에 연결
-
전체 경로 검증
-
실패 사례 및 강화 (Hardening)
-
제한 사항 및 다음 단계
무엇을 구축하게 될 것인가
Model Context Protocol (보통 MCP로 약칭)은 애플리케이션이 AI 시스템에 도구 (tools)와 컨텍스트 (context)를 제시할 수 있는 표준화된 방법을 제공합니다. 각 에이전트 프레임워크 (agent framework)마다 별도의 어댑터 (adapter)를 작성하는 대신, 서버로서 통합을 한 번만 구현하면 호환 가능한 클라이언트 (clients)가 이를 소비할 수 있습니다.
구체적인 사례는 작은 이슈 트래커 (issue-tracker) 통합입니다. 에이전트 (agent)가 프로젝트 키 (project key)를 수신하면 해결되지 않은 이슈를 찾아야 합니다. 상위 서비스 (upstream service)는 이미 다음과 같은 HTTP API를 제공하고 있습니다:
GET /v1/issues?project=OPS&status=open&limit=10
여러분은 이 엔드포인트 (endpoint)를 search_open_issues라는 이름의 도구 (tool)로 래핑 (wrap)하게 됩니다. 이 도구는 입력을 검증하고, API를 호출하며, 응답을 정규화 (normalize)하고, 모델 (model)이 사용할 수 있는 압축된 결과를 반환합니다. 동일한 서버를 비즈니스 로직 (business logic)을 변경하지 않고도 다양한 MCP 호환 호스트 (MCP-compatible hosts)에 연결할 수 있습니다.
예상 결과
마지막에는 실제 HTTP 경계 (HTTP boundary)를 통한 작동하는 서버, 결정론적인 클라이언트 측 테스트 (deterministic client-side test), 그리고 도구를 발견하고 호출하는 예시 에이전트 루프 (agent loop)를 갖게 될 것입니다.
이 튜토리얼은 TypeScript와 Node.js를 사용합니다. API URL과 토큰은 환경 변수 (environment variables)를 통해 제공되므로, 소스 코드에 자격 증명이 포함되지 않습니다. 예제에서는 특정 이슈 트래커 (issue-tracker) 벤더를 가정하지 않도록 의도적으로 설계되었습니다.
아키텍처 및 프로토콜 경계 (Architecture and protocol boundary)
LLM은 이슈 트래커를 직접 호출하지 않습니다. 호스트 애플리케이션 (host application)이 모델과 MCP 클라이언트 (MCP client)를 실행합니다. 해당 클라이언트는 귀하의 서버에 연결하여 도구 (tools)를 발견하고, 승인된 호출을 전달합니다.
User
|
v
...
이러한 분리는 매우 중요합니다:
-
API 클라이언트 (API client)는 인증 (authentication), 타임아웃 (timeouts), HTTP 상태 처리 (HTTP status handling), 그리고 응답 정규화 (response normalization)를 담당합니다.
-
MCP 서버 (MCP server)는 도구 발견 (tool discovery), 입력 유효성 검사 (input validation), 도구 설명 (tool descriptions), 그리고 프로토콜 형태의 결과 (protocol-shaped results)를 담당합니다.
-
에이전트 호스트 (agent host)는 모델 프롬프트 (model prompts), 도구 호출 승인 (tool-call approval), 대화 상태 (conversation state), 그리고 중단 조건 (stopping conditions)을 담당합니다.
이것이 모델 컨텍스트 프로토콜 (Model Context Protocol) 서버의 실질적인 가치입니다. 프로토콜 호환성이 벤더별 API 동작과 분리되어 유지됩니다. 나중에 에이전트 프레임워크 (agent frameworks)를 변경하더라도, 새로운 호스트가 MCP를 지원하는 한 이슈 트래커 어댑터 (issue-tracker adapter)는 변경되지 않고 그대로 유지됩니다.
먼저 로컬 표준 입력/출력 전송 (standard-input/output transport) 방식을 사용할 것입니다. 비즈니스 통합 단계에서는 HTTP 서비스를 호출하지만, 호스트와 서버 간의 연결은 로컬로 유지되어 디버깅이 용이합니다. 도구 구현을 변경하지 않고도 나중에 원격 전송 (remote transport) 방식을 도입할 수 있습니다.
프로토콜 출력을 깨끗하게 유지하기
서버가 표준 출력 (standard output)을 통해 통신할 때는 절대로 그곳에 애플리케이션 로그를 작성하지 마세요. 잘못 들어간 로그 한 줄이 프로토콜 메시지를 손상시킬 수 있습니다. 진단 정보 (diagnostics)는 표준 에러 (standard error)로 보내세요.
프로젝트 준비하기
지원되는 Node.js 릴리스와 최신 버전의 MCP TypeScript SDK를 사용하세요. 패키지 API는 진화할 수 있으므로, 락파일 (lockfile)에 의존성 버전을 고정하고 컴파일러를 설치된 SDK의 최종 권위자로 취급하십시오.
mkdir issue-mcp-server
cd issue-mcp-server
npm init -y
...
Node가 컴파일된 JavaScript를 ES 모듈 (ES modules)로 취급하도록 package.json을 업데이트합니다:
{
"name": "issue-mcp-server",
"private": true,
...
버전을 임의의 숫자로 직접 교체하지 마세요. 설치 후에는 npm이 기록한 정확한 버전을 유지하고, 결과로 생성된 lockfile을 커밋하세요.
간결한 tsconfig.json이면 충분합니다:
{
"compilerOptions": {
"target": "ES2022",
...
디렉토리를 생성합니다:
mkdir -p src test examples
프로젝트 구성은 다음과 같습니다:
issue-mcp-server/
├── examples/
│ └── agent.ts
...
HTTP API 클라이언트 구축
프로토콜 계층(protocol layer) 아래부터 시작합니다. 이렇게 하면 API 동작을 독립적으로 테스트할 수 있으며, 전송 세부 사항(transport details)이 도구 핸들러(tool handlers)로 유출되는 것을 방지할 수 있습니다.
src/api.ts를 생성합니다:
export type Issue = {
id: string;
title: "string;
...
왜 응답을 정규화(normalize)해야 하나요?
HTTP 상태 코드가 성공이라고 해서 본문(body)이 예상된 형태를 갖추고 있다는 것을 증명하지는 않습니다. 검증되지 않은 업스트림(upstream) 데이터를 그대로 반환하면 두 가지 문제가 발생합니다. 에이전트가 불안정한 필드를 받게 되고, 악의적이거나 실수로 포함된 콘텐츠가 과도한 컨텍스트(context)를 소비할 수 있습니다. 정규화는 좁은 데이터 계약(data contract)을 생성하고 잘못된 형식의 레코드를 제거합니다.
토큰은 오직 인증 헤더(authorization header)에서만 사용됩니다. 모델로 반환되거나, 에러에 포함되거나, 로그에 출력되지 않습니다. 진단(diagnostics) 기능을 추가할 때 이 속성을 유지하세요.
API 설정
서버를 실행하는 셸(shell)에서 변수를 설정합니다:
export ISSUES_API_URL="https://issues.example.internal"
export ISSUES_API_TOKEN="replace-with-a-real-token"
실제 API 오리진(origin)을 사용하세요. 토큰을 커밋하거나 공유될 가능성이 있는 에이전트 설정 내부에 두지 마세요. 프로덕션 환경에서는 프로세스 슈퍼바이저(process supervisor) 또는 배포 플랫폼의 비밀 관리(secret-management) 기능을 사용하세요.
MCP 서버 구현
MCP 도구(tool)는 이름, 모델 대상 설명(model-facing description), 입력 스키마(input schema), 그리고 실행 핸들러(execution handler)를 결합합니다. 좋은 설명은 운영 중심적입니다. 즉, 언제 도구를 호출해야 하는지, 무엇을 반환하는지, 그리고 무엇을 하지 않는지를 설명해야 합니다.
src/server.ts를 생성합니다:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from
"@modelcontextprotocol/sdk/server/stdio.js";
...
텍스트 결과는 도구의 텍스트 출력만을 모델에 전달하는 호스트(host)에게 여전히 유용합니다. 구조화된 결과(structured result)는 역량 있는 호스트에게 렌더링 또는 후속 처리를 위한 예측 가능한 객체를 제공합니다.
에러 문구에 주목하세요. 사용할 수 없는 API는 이슈 목록이 비어 있는 것과 동일하지 않습니다. 실패를 "이슈 없음"으로 해석하지 말라고 에이전트(agent)에게 명시적으로 알려줌으로써, 미묘하지만 치명적인 잘못된 결론을 방지할 수 있습니다.
네트워크를 넘기기 전에 검증하기
프로젝트 키(project key)는 40자로 제한되며 보수적인 문자 집합을 사용합니다. 제한은 25자를 초과할 수 없습니다. 이는 단순한 외관상의 확인이 아닙니다. 응답 크기를 제한하고, 모호한 입력을 조기에 거부하며, 업스트림 서비스(upstream service)의 작업 부하를 줄여줍니다.
프로젝트의 타입을 체크하세요:
npm run check
설치한 SDK 버전이 약간 다른 등록 시그니처(registration signature)를 사용하는 경우, 해당 컴파일러 진단(compiler diagnostics)과 설치된 타입 정의(type definitions)를 따르십시오. 아키텍처 경계(architectural boundary)를 그대로 유지하세요: 스키마(schema) 및 프로토콜 로직은 server.ts에, HTTP 동작은 api.ts에 둡니다.
클라이언트로 서버 테스트하기
아직 모델을 도입하지 마세요. 직접적인 MCP 클라이언트는 초기화, 발견(discovery), 검증, 실행 및 종료가 작동한다는 결정론적인 증거를 생성합니다.
test/client.ts를 생성합니다:
import { Client } from
"@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from
...
접근 권한이 있는 엔드포인트(endpoint)를 구성한 후에만 실행하세요:
npm run test:client
이것은 통합 확인(integration check)이며, 특정 출력에 대한 약속이 아닙니다. 성공적인 실행은 다음 사항을 모두 입증해야 합니다:
- 클라이언트가 프로토콜 핸드셰이크(protocol handshake)를 완료합니다.
listTools에search_open_issues가 포함되어 있습니다.- 호출이 구성된 API에 도달합니다.
- 결과에 텍스트가 포함되어 있으며, 지원되는 경우 구조화된 콘텐츠(structured content)가 포함되어 있습니다.
- 프로세스가 멈추지(hanging) 않고 종료됩니다.
프로덕션 서비스 없이 테스트하기
반복 가능한 자동화 테스트를 위해, 동일한 엔드포인트(endpoint)를 구현하는 로컬 테스트 서버로 ISSUES_API_URL을 지정하세요. 해당 피스처(fixture)는 의도적으로 작은 응답을 반환할 수 있습니다:
{
"issues": [
{
...
.invalid로 끝나는 도메인은 피스처가 실제 고객이나 라이브 서비스를 암시하는 것을 방지합니다. 테스트는 단순히 결과를 출력하는 것이 아니라, 데이터의 형태(shape)와 변환(transformation)을 검증(assert)해야 합니다. 유용한 검증(assertion) 항목은 다음과 같습니다:
- 업스트림(upstream) 요청에
status=open이 포함되어 있는지 확인. - 베어러 헤더(bearer header)가 존재하지만, 그 값이 로그에 남지 않는지 확인.
limit를 3으로 설정했을 때, 정규화된 이슈(normalized issues)가 3개를 초과하지 않는지 확인.- 잘못된 형식의 레코드(malformed records)가 제외되는지 확인.
- HTTP 401, 429, 500 응답 시
isError: true가 생성되는지 확인. - 잘못된 입력(invalid input)이 HTTP 요청이 발생하기 전에 거부되는지 확인.
입력 유효성 검사(Input validation) 조사
클라이언트 인자(arguments)를 각 유효하지 않은 케이스로 임시 변경해 보세요:
{ "project": "", "limit": 3 }
{ "project": "../admin", "limit": 3 }
{ "project": "OPS", "limit": 0 }
...
각 호출은 API에 도달하기 전에 유효성 검사(validation)에서 실패해야 합니다. 정확한 에러 포맷팅은 SDK와 클라이언트에 따라 다르므로, 취약한 텍스트 문구 대신 실패 카테고리를 검증(assert)하세요.
서버를 LLM 에이전트에 연결하기
두 가지 실용적인 연결 패턴이 있습니다. 데스크톱 또는 IDE 호스트는 설정을 통해 서버를 실행할 수 있습니다. 커스텀 애플리케이션은 직접 클라이언트 세션을 열고 발견된 도구(tools)를 모델 SDK에 노출할 수 있습니다.
옵션 A: MCP 호환 호스트 설정하기
전형적인 로컬 호스트 설정은 다음과 같은 형태를 가집니다:
{
"mcpServers": {
"issue-search": {
...
호스트를 dist로 지정하기 전에 빌드하세요:
npm run build
설정 파일 이름과 계층 구조는 호스트마다 다릅니다. 외부 설정을 사용 중인 호스트에 맞게 조정하되, 실행 파일(executable), 인자(arguments), 환경(environment)의 의미론(semantics)은 유지하세요. 호스트를 재시작하거나 다시 로드한 후, 도구 목록(tool list)을 검사하여 자연어 요청을 보내기 전에 search_open_issues가 나타나는지 확인하세요.
Claude 모델 컨텍스트 프로토콜 (Claude model context protocol)이라는 문구는 설정 관련 검색에서 자주 등장하는데, 이는 Claude 호환 호스트(hosts)들이 MCP를 대중화하는 데 기여했기 때문입니다. 여기서 구축하는 서버는 특정 모델 벤더(vendor)에 종속되지 않습니다. 호환성은 호스트의 프로토콜 지원 및 전송(transport) 구성에 달려 있습니다.
옵션 B: 커스텀 에이전트(custom agent)에 연결하기
커스텀 에이전트 루프(agent loop)에는 네 가지 필수 작업이 있습니다:
- MCP 클라이언트를 연결하고 도구 목록(tool list)을 나열합니다.
- 도구의 스키마(schemas)를 모델 제공자(model provider)의 도구 형식으로 변환합니다.
- 사용자 요청과 도구 정의를 모델에 전송합니다.
- MCP를 통해 요청된 호출을 실행하고 그 결과를 모델에 반환합니다.
다음의 제공자 중립적(provider-neutral) 스켈레톤(skeleton) 코드는 제어 흐름(control flow)을 보여줍니다. 플레이스홀더(placeholder)인 모델 어댑터(model adapter)를 이미 사용 중인 SDK로 교체하세요:
import { Client } from
"@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from
"@modelcontextprotocol/sdk/client/stdio.js";
type ModelToolCall = {
id: string;
name: string;
arguments: Record<string, unknown>;
};
type ModelResponse = {
text?: string;
toolCalls: ModelToolCall[];
};
async function callModel(input: {
messages: unknown[];
tools: unknown[];
}): Promise<ModelResponse> {
throw new Error(
"Implement this adapter with your chosen model SDK"
);
}
const transport = new StdioClientTransport({
command: "node",
args: ["dist/src/server.js"],
env: {
...process.env,
ISSUES_API_URL:
process.env.ISSUES_API_URL ?? "",
ISSUES_API_TOKEN:
process.env.ISSUES_API_TOKEN ?? ""
}
});
const mcp = new Client({
name: "issue-agent",
version: "1.0.0"
});
await mcp.connect(transport);
try {
const { tools } = await mcp.listTools();
const modelTools = tools.map((tool) => ({
type: "function",
name: tool.name,
description: "tool.description,",
parameters: tool.inputSchema
}));
const messages: unknown[] = [
{
role: "system",
content:
"현재 이슈 데이터를 위해 도구(tools)를 사용하세요. 도구 오류를 " +
"빈 결과로 취급하지 마세요. 이슈의 세부 사항을 임의로 만들어내지 마세요."
},
{
role: "user",
content:
"OPS에서 열려 있는 이슈를 최대 3개까지 나열하고 요약해 주세요."
}
];
for (let turn = 0; turn < 6; turn += 1) {
const response = await callModel({
messages,
tools: modelTools
});
if (response.toolCalls.length === 0) {
console.log(response.text ?? "");
break;
}
messages.push({
role: "assistant",
content: response
});
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기