안녕하세요! 지난 포스팅에서 MCP의 기본 개념을 살펴봤는데요, 오늘은 본격적으로 MCP 서버를 구현하는 방법에 대해 자세히 알아보겠습니다. 직접 따라하며 Claude와 함께하는 강력한 AI 시스템을 구축해보세요! 😊
MCP 서버 아키텍처 이해하기 🏗️
MCP 서버는 Claude와 외부 시스템 사이의 중개자 역할을 합니다. 서버는 크게 다음과 같은 구성요소로 이루어져 있습니다:
- 라우터(Router): 클라이언트의 요청을 적절한 핸들러로 전달합니다
- 핸들러(Handler): 실제 작업을 수행하는 함수들입니다
- 인증(Authentication): 요청이 유효한지 확인합니다
- 로깅(Logging): 서버 활동을 기록합니다
- 에러 처리(Error Handling): 오류 상황을 관리합니다

Node.js로 MCP 서버 구현하기 🚀
이제 Node.js를 사용하여 간단한 MCP 서버를 구현해보겠습니다. 이 서버는 파일 시스템과 상호작용하고 웹 검색을 수행할 수 있는 기능을 포함합니다.
1. 프로젝트 설정
먼저 필요한 패키지를 설치하고 기본 프로젝트 구조를 만들어 보겠습니다.
// 프로젝트 디렉토리 생성 및 초기화
// 터미널에서 다음 명령어 실행:
mkdir claude-mcp-server
cd claude-mcp-server
npm init -y
// 필요한 패키지 설치
npm install express cors dotenv axios fs-extra
// 프로젝트 구조
/*
claude-mcp-server/
├── .env # 환경 변수 파일
├── package.json # 패키지 정보
├── index.js # 서버 진입점
├── handlers/ # 핸들러 함수들
│ ├── fileHandler.js # 파일 시스템 핸들러
│ └── searchHandler.js # 웹 검색 핸들러
└── utils/ # 유틸리티 함수들
├── auth.js # 인증 관련 유틸리티
└── logger.js # 로깅 유틸리티
*/
2. 환경 변수 설정 (.env 파일)
보안 키와 설정을 관리하기 위한 환경 변수 파일을 생성합니다.
# MCP 서버 설정
PORT=3000
MCP_API_KEY=your_secure_api_key_here
# 파일 시스템 설정
BASE_DIR=/path/to/allowed/directory
# 검색 API 설정 (예: 브레이브 검색)
SEARCH_API_KEY=your_search_api_key_here
SEARCH_API_URL=https://api.search.brave.com/res/v1/web/search
3. 인증 및 로깅 유틸리티 구현
보안과 로깅을 위한 유틸리티 함수를 만들어 보겠습니다.
// utils/auth.js
// 인증 미들웨어
require('dotenv').config();
const authenticateRequest = (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.MCP_API_KEY) {
console.log('인증 실패: 유효하지 않은 API 키');
return res.status(401).json({
error: '인증 실패: 유효하지 않은 API 키입니다.'
});
}
// 인증 성공
next();
};
module.exports = { authenticateRequest };
// utils/logger.js
// 간단한 로깅 시스템
const fs = require('fs-extra');
const path = require('path');
// 로그 디렉토리 확인 및 생성
const logDir = path.join(__dirname, '../logs');
fs.ensureDirSync(logDir);
const logFilePath = path.join(logDir, `mcp-${new Date().toISOString().split('T')[0]}.log`);
const logLevels = {
ERROR: 'ERROR',
WARN: 'WARN',
INFO: 'INFO',
DEBUG: 'DEBUG'
};
const logger = {
log: (level, message, data = {}) => {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
data
};
// 콘솔에 로그 출력
console.log(`[${timestamp}] [${level}] ${message}`, data);
// 파일에 로그 저장
fs.appendFileSync(
logFilePath,
`${JSON.stringify(logEntry)}\n`,
{ encoding: 'utf8' }
);
},
error: (message, data) => logger.log(logLevels.ERROR, message, data),
warn: (message, data) => logger.log(logLevels.WARN, message, data),
info: (message, data) => logger.log(logLevels.INFO, message, data),
debug: (message, data) => logger.log(logLevels.DEBUG, message, data),
};
module.exports = logger;
4. 파일 시스템 핸들러 구현
Claude가 로컬 파일 시스템과 상호작용할 수 있도록 핸들러를 구현합니다.
// handlers/fileHandler.js
const fs = require('fs-extra');
const path = require('path');
const logger = require('../utils/logger');
require('dotenv').config();
// 허용된 기본 디렉토리
const BASE_DIR = process.env.BASE_DIR || path.join(__dirname, '../data');
// 경로가 허용된 디렉토리 내에 있는지 확인
const isPathSafe = (requestedPath) => {
// 절대 경로로 변환
const absolutePath = path.resolve(requestedPath);
const baseDirPath = path.resolve(BASE_DIR);
// 경로가 기본 디렉토리 내에 있는지 확인
return absolutePath.startsWith(baseDirPath);
};
// 파일 정보 가져오기
const getFileInfo = async (req, res) => {
try {
const { filePath } = req.body;
if (!filePath) {
return res.status(400).json({ error: '파일 경로가 필요합니다.' });
}
const fullPath = path.join(BASE_DIR, filePath);
// 경로 안전성 검사
if (!isPathSafe(fullPath)) {
logger.warn('안전하지 않은 경로 접근 시도', { path: fullPath });
return res.status(403).json({ error: '허용되지 않은 디렉토리 접근입니다.' });
}
// 파일 존재 확인
const exists = await fs.pathExists(fullPath);
if (!exists) {
return res.status(404).json({ error: '파일이 존재하지 않습니다.' });
}
// 파일 정보 가져오기
const stats = await fs.stat(fullPath);
const isDirectory = stats.isDirectory();
const fileInfo = {
name: path.basename(fullPath),
path: filePath,
size: stats.size,
isDirectory,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime
};
logger.info('파일 정보 요청 성공', { path: filePath });
return res.json(fileInfo);
} catch (error) {
logger.error('파일 정보 가져오기 실패', { error: error.message });
return res.status(500).json({ error: `파일 정보를 가져올 수 없습니다: ${error.message}` });
}
};
// 파일 읽기
const readFile = async (req, res) => {
try {
const { filePath } = req.body;
if (!filePath) {
return res.status(400).json({ error: '파일 경로가 필요합니다.' });
}
const fullPath = path.join(BASE_DIR, filePath);
// 경로 안전성 검사
if (!isPathSafe(fullPath)) {
logger.warn('안전하지 않은 경로 접근 시도', { path: fullPath });
return res.status(403).json({ error: '허용되지 않은 디렉토리 접근입니다.' });
}
// 파일 존재 확인
const exists = await fs.pathExists(fullPath);
if (!exists) {
return res.status(404).json({ error: '파일이 존재하지 않습니다.' });
}
// 파일 상태 확인
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
return res.status(400).json({ error: '디렉토리가 아닌 파일 경로가 필요합니다.' });
}
// 파일 읽기
const content = await fs.readFile(fullPath, 'utf8');
logger.info('파일 읽기 성공', { path: filePath });
return res.json({ content });
} catch (error) {
logger.error('파일 읽기 실패', { error: error.message });
return res.status(500).json({ error: `파일을 읽을 수 없습니다: ${error.message}` });
}
};
// 파일 쓰기
const writeFile = async (req, res) => {
try {
const { filePath, content } = req.body;
if (!filePath || content === undefined) {
return res.status(400).json({ error: '파일 경로와 내용이 필요합니다.' });
}
const fullPath = path.join(BASE_DIR, filePath);
// 경로 안전성 검사
if (!isPathSafe(fullPath)) {
logger.warn('안전하지 않은 경로 접근 시도', { path: fullPath });
return res.status(403).json({ error: '허용되지 않은 디렉토리 접근입니다.' });
}
// 디렉토리 생성 (필요한 경우)
await fs.ensureDir(path.dirname(fullPath));
// 파일 쓰기
await fs.writeFile(fullPath, content, 'utf8');
logger.info('파일 쓰기 성공', { path: filePath });
return res.json({ success: true, message: '파일이 성공적으로 작성되었습니다.' });
} catch (error) {
logger.error('파일 쓰기 실패', { error: error.message });
return res.status(500).json({ error: `파일을 쓸 수 없습니다: ${error.message}` });
}
};
// 디렉토리 목록 가져오기
const listDirectory = async (req, res) => {
try {
const { dirPath } = req.body;
// 기본 경로 사용 (dirPath가 없는 경우)
const relativePath = dirPath || '';
const fullPath = path.join(BASE_DIR, relativePath);
// 경로 안전성 검사
if (!isPathSafe(fullPath)) {
logger.warn('안전하지 않은 경로 접근 시도', { path: fullPath });
return res.status(403).json({ error: '허용되지 않은 디렉토리 접근입니다.' });
}
// 디렉토리 존재 확인
const exists = await fs.pathExists(fullPath);
if (!exists) {
return res.status(404).json({ error: '디렉토리가 존재하지 않습니다.' });
}
// 디렉토리 확인
const stats = await fs.stat(fullPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: '해당 경로는 디렉토리가 아닙니다.' });
}
// 디렉토리 내용 읽기
const items = await fs.readdir(fullPath);
// 각 항목의 정보 가져오기
const itemDetails = await Promise.all(
items.map(async (item) => {
const itemPath = path.join(fullPath, item);
const itemStats = await fs.stat(itemPath);
return {
name: item,
path: path.join(relativePath, item),
isDirectory: itemStats.isDirectory(),
size: itemStats.size,
modified: itemStats.mtime
};
})
);
logger.info('디렉토리 목록 요청 성공', { path: dirPath });
return res.json({ items: itemDetails });
} catch (error) {
logger.error('디렉토리 목록 가져오기 실패', { error: error.message });
return res.status(500).json({ error: `디렉토리 목록을 가져올 수 없습니다: ${error.message}` });
}
};
module.exports = {
getFileInfo,
readFile,
writeFile,
listDirectory
};
5. 웹 검색 핸들러 구현
Claude가 인터넷 검색을 수행할 수 있도록 검색 기능을 구현합니다.
// handlers/searchHandler.js
const axios = require('axios');
const logger = require('../utils/logger');
require('dotenv').config();
// 웹 검색 수행
const performWebSearch = async (req, res) => {
try {
const { query, count = 10 } = req.body;
if (!query) {
return res.status(400).json({ error: '검색어가 필요합니다.' });
}
// 검색어 길이 제한 (예: 최대 100자)
if (query.length > 100) {
return res.status(400).json({ error: '검색어는 100자 이내여야 합니다.' });
}
// 결과 개수 유효성 검사
const resultCount = Math.min(Math.max(1, count), 50); // 1~50개 사이 제한
// 브레이브 검색 API 호출 (예시)
const response = await axios({
method: 'GET',
url: process.env.SEARCH_API_URL,
headers: {
'Accept': 'application/json',
'X-API-Key': process.env.SEARCH_API_KEY
},
params: {
q: query,
count: resultCount
}
});
// 검색 결과 가공 (실제 API 응답 구조에 맞게 조정 필요)
const searchResults = response.data.results || [];
// 필요한 정보만 추출하여 반환
const formattedResults = searchResults.map(result => ({
title: result.title,
description: result.description,
url: result.url,
publishedDate: result.publishedDate
}));
logger.info('웹 검색 성공', { query, resultCount: formattedResults.length });
return res.json({ results: formattedResults });
} catch (error) {
logger.error('웹 검색 실패', { error: error.message });
// API 오류 응답 처리
if (error.response) {
return res.status(error.response.status).json({
error: `검색 API 오류: ${error.response.data.message || error.message}`
});
}
return res.status(500).json({ error: `검색을 수행할 수 없습니다: ${error.message}` });
}
};
module.exports = {
performWebSearch
};
6. 메인 서버 파일 구현
이제 모든 핸들러를 통합하여 MCP 서버의 메인 파일을 만들어 보겠습니다.
// index.js - MCP 서버 메인 파일
const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs-extra');
const { authenticateRequest } = require('./utils/auth');
const logger = require('./utils/logger');
const fileHandler = require('./handlers/fileHandler');
const searchHandler = require('./handlers/searchHandler');
require('dotenv').config();
// Express 앱 생성
const app = express();
const PORT = process.env.PORT || 3000;
// 기본 디렉토리 확인/생성
const BASE_DIR = process.env.BASE_DIR || path.join(__dirname, 'data');
fs.ensureDirSync(BASE_DIR);
// 미들웨어 설정
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 서버 상태 확인 엔드포인트 (인증 필요 없음)
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 서버 정보 엔드포인트 (MCP 사양 정보)
app.get('/.well-known/mcp-server-info', (req, res) => {
res.json({
name: "Claude MCP Server",
version: "1.0.0",
description: "MCP 서버 - 파일 시스템 및 웹 검색 기능",
capabilities: [
"file_operations",
"web_search"
],
endpoints: {
"file/info": "파일 정보 조회",
"file/read": "파일 읽기",
"file/write": "파일 쓰기",
"file/list": "디렉토리 목록 조회",
"search/web": "웹 검색 수행"
}
});
});
// 파일 시스템 엔드포인트 (인증 필요)
app.post('/file/info', authenticateRequest, fileHandler.getFileInfo);
app.post('/file/read', authenticateRequest, fileHandler.readFile);
app.post('/file/write', authenticateRequest, fileHandler.writeFile);
app.post('/file/list', authenticateRequest, fileHandler.listDirectory);
// 검색 엔드포인트 (인증 필요)
app.post('/search/web', authenticateRequest, searchHandler.performWebSearch);
// 오류 처리 미들웨어
app.use((err, req, res, next) => {
logger.error('서버 오류', { error: err.message, stack: err.stack });
res.status(500).json({ error: '서버 내부 오류가 발생했습니다.' });
});
// 404 Not Found 처리
app.use((req, res) => {
logger.warn('존재하지 않는 엔드포인트 접근', { path: req.originalUrl });
res.status(404).json({ error: '요청한 리소스를 찾을 수 없습니다.' });
});
// 서버 시작
app.listen(PORT, () => {
logger.info(`MCP 서버가 포트 ${PORT}에서 실행 중입니다.`);
logger.info(`기본 디렉토리: ${BASE_DIR}`);
console.log(`MCP 서버가 포트 ${PORT}에서 실행 중입니다.`);
});
Claude와 MCP 서버 연결하기 🔄
이제 구현한 MCP 서버를 Claude Desktop과 연결하는 방법을 알아보겠습니다.
- 서버 실행: 터미널에서
node index.js명령어로 서버를 실행합니다. - Claude Desktop 설정:
Claude Desktop에서 MCP 서버를 연결하려면 다음 단계를 따릅니다:
- Claude Desktop 앱을 실행합니다
- 설정 메뉴로 이동합니다 (일반적으로 왼쪽 하단 설정 아이콘)
- "MCP 서버" 또는 "개발자 설정" 섹션을 찾습니다
- "새 서버 추가" 버튼을 클릭합니다
- 서버 URL(예:
http://localhost:3000)과 API 키를 입력합니다 - "연결 테스트" 버튼을 클릭하여 연결을 확인합니다
- 연결이 성공하면 "저장" 버튼을 클릭합니다

MCP 서버 테스트 및 디버깅 🔍
MCP 서버를 효과적으로 테스트하려면 다음 방법을 사용할 수 있습니다:
1. Postman이나 curl을 사용한 API 테스트
서버가 올바르게 작동하는지 먼저 API 클라이언트를 통해 확인합니다:
# 서버 상태 확인 (인증 필요 없음)
curl http://localhost:3000/health
# 서버 정보 확인 (인증 필요 없음)
curl http://localhost:3000/.well-known/mcp-server-info
# 디렉토리 목록 조회 (인증 필요)
curl -X POST \
http://localhost:3000/file/list \
-H "Content-Type: application/json" \
-H "X-API-Key: your_secure_api_key_here" \
-d '{"dirPath": ""}'
# 파일 읽기 (인증 필요)
curl -X POST \
http://localhost:3000/file/read \
-H "Content-Type: application/json" \
-H "X-API-Key: your_secure_api_key_here" \
-d '{"filePath": "example.txt"}'
# 파일 쓰기 (인증 필요)
curl -X POST \
http://localhost:3000/file/write \
-H "Content-Type: application/json" \
-H "X-API-Key: your_secure_api_key_here" \
-d '{"filePath": "new-example.txt", "content": "Hello, MCP World!"}'
# 웹 검색 (인증 필요)
curl -X POST \
http://localhost:3000/search/web \
-H "Content-Type: application/json" \
-H "X-API-Key: your_secure_api_key_here" \
-d '{"query": "MCP 서버 Claude", "count": 5}'
2. 로그 확인 및 디버깅
서버 실행 중 발생하는 문제를 해결하기 위한 디버깅 팁:
- 로그 확인:
logs디렉토리에서 생성된 로그 파일을 확인합니다. - 콘솔 출력: 터미널 창에서 실시간 로그 메시지를 모니터링합니다.
- API 응답 검증: 반환된 응답이 예상과 일치하는지 확인합니다.
- 권한 문제: 파일 시스템 작업 시 파일 및 디렉토리 권한을 확인합니다.
3. Claude를 통한 통합 테스트
Claude와 MCP 서버가 연결된 후, 다음과 같은 명령을 사용하여 통합 테스트를 수행할 수 있습니다:
- 파일 읽기: "내 컴퓨터에서 example.txt 파일을 읽어주세요."
- 디렉토리 탐색: "내 문서 폴더에 어떤 파일이 있는지 알려주세요."
- 웹 검색: "MCP 서버에 대한 최신 정보를 검색해주세요."
- 파일 작성: "다음 내용을 test.txt 파일로 저장해주세요: [내용]"
다음 포스팅 예고 🔮
다음 포스팅에서는 MCP 서버를 확장하는 방법에 대해 알아보겠습니다. 데이터베이스 연결, API 통합, 고급 보안 설정 등 MCP 서버를 더욱 강력하게 만드는 방법을 소개해 드리겠습니다.
마무리 🌟
이번 포스팅에서는 Node.js를 사용하여 기본적인 MCP 서버를 구현하고 Claude Desktop과 연결하는 방법을 살펴보았습니다. 파일 시스템 핸들러와 웹 검색 기능을 통해 Claude의 능력을 크게 확장시킬 수 있습니다.
MCP 서버 구현은:
- 프로젝트 설정 및 의존성 설치
- 핵심 유틸리티 작성 (인증, 로깅)
- 핸들러 구현 (파일 시스템, 웹 검색)
- 메인 서버 파일 작성
- Claude와 연결 및 테스트
이러한 단계로 진행됩니다.
다음 포스팅에서 더 많은 고급 기능에 대해 알아보겠습니다. 질문이나 의견이 있으시면 댓글로 남겨주세요! 😊
'개발' 카테고리의 다른 글
| Claude를 활용한 MCP(Model Context Protocol) 설정 가이드 - Part 4: 실전 업무 자동화 응용 💼 (0) | 2025.03.29 |
|---|---|
| Claude를 활용한 MCP(Model Context Protocol) 설정 가이드 - Part 3: MCP 서버 확장하기 🚀 (0) | 2025.03.28 |
| Claude를 활용한 MCP(Model Context Protocol) 설정 가이드 - Part 1: 기초 개념 이해하기 🚀 (0) | 2025.03.25 |
| 🌈 PyCharm으로 시작하는 FastAPI 개발 여정 (초보자를 위한 가이드) 4편 ✨ (0) | 2025.03.21 |
| PyCharm으로 시작하는 FastAPI 개발 여정 (초보자를 위한 가이드) 3편 ✨ (0) | 2025.03.20 |