TypeScript로 MCP 서버 만들기: Hermes AI 에이전트에 PostgreSQL·Grafana를 연결하는 법
온콜 당번이 돌아왔던 어느 새벽, 슬랙에 이런 알림이 떴습니다. "결제 서비스 5xx 급증, 원인 불명." 대시보드 열고, DB 접속하고, 쿼리 짜고, Grafana 패널 뒤지고... 원인을 파악하는 데만 꼬박 30분이 걸렸습니다. 그 뒤 MCP를 도입하고 나서는 같은 상황에서 15분으로 줄었습니다. Hermes Agent에게 "지난 1시간 동안 API 응답 시간이 500ms를 넘은 서비스가 뭐야?"라고 물어보면 알아서 Prometheus에 PromQL을 날리고, DB를 조회하고, 분석 결과까지 내놓으니까요.
저도 처음엔 "AI랑 내부 시스템을 연결하려면 LangChain으로 복잡하게 짜야 하나" 싶었는데, MCP(Model Context Protocol) 서버 하나로 Hermes Agent가 PostgreSQL과 Grafana까지 직접 다루게 만들 수 있습니다. M×N으로 불어나던 연동 코드를 M+N으로 줄여주는 이 구조가, 생각보다 훨씬 빠르게 시작해볼 수 있습니다.
이 글에서는 MCP가 무엇인지부터 시작해서, TypeScript로 커스텀 PostgreSQL MCP 서버를 만드는 법, 그리고 Prometheus와 Grafana를 Hermes Agent에 연결하는 설정까지 다룹니다. 사전 준비: Node.js 18+, pnpm, 접근 가능한 PostgreSQL 개발 DB, 그리고 MCP 클라이언트 모드를 지원하는 Hermes Agent v0.2.0+ — 설치 방법은 공식 문서를 참고해 주세요. 이 환경이 갖춰져 있다면 오늘 안에 로컬에서 동작하는 걸 확인할 수 있습니다.
핵심 개념
MCP가 "AI의 USB-C"라 불리는 이유
MCP는 Anthropic이 2024년 11월에 공개한 개방형 표준입니다. AI 모델이 외부 시스템과 대화하는 방식을 통일한 프로토콜이라고 이해하면 됩니다.
기존에는 AI를 내부 시스템에 연결할 때마다 커스텀 인터페이스를 새로 만들어야 했습니다. DB 하나, Slack 하나, Jira 하나... 시스템이 늘어날수록 연동 코드도 M×N으로 불어났죠. MCP는 이 문제를 M+N으로 바꿉니다. MCP 서버를 한 번 만들어두면 Hermes Agent뿐 아니라 Claude Code, Cursor, 앞으로 나올 어떤 MCP 호환 클라이언트에서도 그대로 재사용할 수 있습니다.
MCP의 세 가지 프리미티브
- Tools: 에이전트가 호출할 수 있는 함수 (SQL 실행, API 호출 등)
- Resources: 에이전트가 읽을 수 있는 데이터 (파일, DB 레코드 등)
- Prompts: 재사용 가능한 프롬프트 템플릿
출시 당시 월 200만 건이던 SDK 다운로드가 2026년 3월 기준 9,700만 건을 넘어섰고, OpenAI·Microsoft·AWS가 모두 공식 채택했습니다. Gartner는 2026년 말까지 API 게이트웨이 벤더의 75%가 MCP를 지원할 것으로 예측하고 있습니다.
Hermes Agent와 MCP의 연결 구조
Hermes Agent는 Nous Research가 만든 오픈소스 자율 AI 에이전트입니다. v0.2.0부터 MCP 클라이언트 모드를 지원해서, config.yaml에 MCP 서버를 등록하기만 하면 에이전트 시작 시 자동으로 핸드셰이크를 수행하고 해당 서버의 도구들을 사용할 수 있게 됩니다.
# hermes config.yaml — 가장 기본적인 구조
mcp_servers:
서버이름:
command: "실행할 명령어"
args: ["인자들"]
allowed_tools:
- "허용할_도구_이름"allowed_tools가 중요합니다. 에이전트가 접근할 수 있는 도구를 이 목록으로 명시적으로 제한할 수 있어서, 최소 권한 원칙을 적용하기에 딱 좋습니다.
트랜스포트 방식: stdio vs Streamable HTTP
MCP 서버를 실행하는 방식은 크게 두 가지입니다.
| 방식 | 특징 | 적합한 상황 |
|---|---|---|
| stdio | 표준 입출력으로 통신, 설정 간단 | 로컬 개발, 클라이언트와 같은 머신 |
| Streamable HTTP | HTTP 기반 원격 통신, 2025년 3월 표준화 | 팀 공유 서버, 프로덕션 배포 |
솔직히 말하면 현재 공식 MCP 서버의 대부분이 아직 stdio 방식입니다. 프로덕션에서 팀 전체가 쓰려면 Streamable HTTP로 가야 하는데, 그쪽은 셋업이 좀 번거롭습니다. 개발·검증 단계에서는 stdio로 시작하는 게 맞습니다.
실전 적용
직접 만들기: 사내 PostgreSQL MCP 서버 구축
postgres-mcp 같은 기성품도 있지만, 사내 DB는 보안 정책이나 쿼리 제한이 제각각이라 직접 만드는 게 나을 때가 많습니다. TypeScript SDK로 만들면 타입 안전성도 확보되고, 보안 레이어를 코드 레벨에서 직접 제어할 수 있습니다.
pnpm init
pnpm add @modelcontextprotocol/sdk pg zod
pnpm add -D typescript @types/pg tsx// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Pool } from "pg";
import { z } from "zod";
// 시작 시점에 환경 변수 검증 — undefined면 Pool 생성 전에 즉시 종료
const DB_URL = process.env.DB_URL;
if (!DB_URL) {
console.error("오류: DB_URL 환경 변수가 설정되지 않았습니다.");
process.exit(1);
}
const server = new McpServer({ name: "internal-db-mcp", version: "1.0.0" });
const pool = new Pool({ connectionString: DB_URL, max: 5 });
// 코드 레벨 참조용 화이트리스트
// 주의: 이 목록만으로는 DB 레벨 접근을 막지 못합니다.
// 실제 접근 제어는 읽기 전용 계정 + 뷰(view) 권한으로 처리해야 합니다.
const ALLOWED_TABLES = ["orders", "services", "error_logs", "metrics"];
function validateSql(sql: string): string | null {
if (!/^\s*SELECT\b/i.test(sql)) return "SELECT 쿼리만 허용됩니다.";
// 세미콜론 차단: "SELECT 1; DROP TABLE orders" 같은 다중 구문 인젝션 방어
if (/;/.test(sql)) return "세미콜론은 허용되지 않습니다.";
// 주석 차단: "--" 또는 "/*" 주석으로 필터를 우회하는 시도 방어
if (/--|\/\*/.test(sql)) return "SQL 주석은 허용되지 않습니다.";
return null;
}
server.tool(
"query_metrics",
"지표 데이터 조회 (읽기 전용)",
{
sql: z.string().describe("실행할 SELECT 쿼리"),
limit: z.number().min(1).max(500).default(100),
},
async ({ sql, limit }) => {
const validationError = validateSql(sql);
if (validationError) {
return {
content: [{ type: "text", text: `오류: ${validationError}` }],
isError: true,
};
}
// 원본 쿼리에 LIMIT이 이미 있으면 중복 추가 시 문법 오류가 발생하므로 분기 처리
const hasLimit = /\bLIMIT\b/i.test(sql);
const finalSql = hasLimit ? sql : `${sql} LIMIT $1`;
const params = hasLimit ? [] : [limit];
try {
const result = await pool.query(finalSql, params);
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
};
} catch (err) {
return {
content: [
{
type: "text",
text: `쿼리 실행 오류: ${err instanceof Error ? err.message : "알 수 없는 오류"}`,
},
],
isError: true,
};
}
}
);
server.tool(
"list_tables",
"조회 가능한 테이블 목록 반환",
{},
async () => ({
content: [{ type: "text", text: ALLOWED_TABLES.join("\n") }],
})
);
server.tool(
"describe_table",
"테이블 스키마 조회",
{
// z.enum()은 최소 하나 이상인 튜플 타입 [string, ...string[]]을 요구합니다.
// TypeScript가 ALLOWED_TABLES를 일반 string[]으로 추론하기 때문에 이 캐스팅이 필요합니다.
table_name: z.enum(ALLOWED_TABLES as [string, ...string[]]),
},
async ({ table_name }) => {
try {
const result = await pool.query(
`SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position`,
[table_name]
);
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
};
} catch (err) {
return {
content: [
{
type: "text",
text: `스키마 조회 오류: ${err instanceof Error ? err.message : "알 수 없는 오류"}`,
},
],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);코드에서 눈여겨볼 부분들입니다.
| 포인트 | 설명 |
|---|---|
시작 시 DB_URL 검증 |
undefined면 Pool 생성 전에 즉시 종료 — 조용한 실패 방지 |
validateSql() 함수 |
SELECT 한정 + 세미콜론 + 주석 차단으로 다중 구문 인젝션 방어 |
| LIMIT 중복 체크 | 원본 쿼리에 LIMIT이 이미 있으면 추가하지 않아 문법 오류 방지 |
| try/catch 처리 | DB 연결 실패·타임아웃·문법 오류 시 에러 응답 반환 |
max: 5 커넥션 풀 |
에이전트가 쿼리를 폭발적으로 보낼 때 DB 부하 제한 |
z.enum(ALLOWED_TABLES) |
Zod 스키마로 테이블명 자체를 타입 레벨에서 검증 |
보안 설계에서 가장 중요한 부분:
validateSql()의 정규식은 보조 방어선입니다. 주 방어선은 PostgreSQL에서 읽기 전용 역할(pg_read_all_data) 전용 계정을 만들고 그 계정으로만 연결하는 것입니다. 코드 레벨 필터가 뚫리더라도 DB 계정 자체에 쓰기 권한이 없으면 데이터 손상은 일어나지 않습니다. 마찬가지로ALLOWED_TABLES목록은describe_table입력 검증에는 유효하지만,query_metrics를 통해 다른 테이블을 조회하는 것을 막지는 못합니다. 특정 테이블만 노출하고 싶다면 DB에서 해당 테이블·뷰에만 SELECT 권한을 부여하는 게 확실한 방법입니다.
이제 Hermes config.yaml에 등록합니다.
# hermes config.yaml
mcp_servers:
internal_db:
command: "pnpm"
args: ["exec", "tsx", "/path/to/internal-db-mcp/src/server.ts"]
env:
DB_URL: "postgresql://readonly_user:pass@db.internal:5432/prod"
allowed_tools:
- "query_metrics"
- "list_tables"
- "describe_table"공식 서버 활용: Grafana + Prometheus 모니터링 연결
모니터링 시스템은 직접 만들 필요 없이 공식·커뮤니티 서버를 쓰는 게 낫습니다. Grafana 공식 MCP 서버와 커뮤니티 Prometheus 서버를 조합하면 됩니다.
PromQL 한 줄 설명: Prometheus에서 메트릭을 조회하는 쿼리 언어입니다.
rate(http_requests_total[5m])처럼 시간 범위를 지정해 시계열 데이터를 집계하며, SQL과 비슷하지만 시계열에 특화되어 있습니다.
# hermes config.yaml — 모니터링 서버 추가
mcp_servers:
internal_db:
command: "pnpm"
args: ["exec", "tsx", "/path/to/internal-db-mcp/src/server.ts"]
env:
DB_URL: "postgresql://readonly_user:pass@db.internal:5432/prod"
allowed_tools:
- "query_metrics"
- "list_tables"
- "describe_table"
grafana:
command: "npx"
args:
- "@grafana/mcp-grafana"
- "--grafana-url"
- "http://grafana.internal:3000"
- "--api-key"
- "${GRAFANA_API_KEY}"
# 주의: ${...} 환경 변수 인터폴레이션은 Hermes 설정 파서가 지원해야 작동합니다.
# 미지원 시 env: 블록에 GRAFANA_API_KEY를 직접 지정하는 방식으로 대체해 주세요.
allowed_tools:
- "list_dashboards"
- "get_dashboard"
- "query_datasource"
- "list_alerts"
prometheus:
command: "python"
args: ["-m", "prometheus_mcp_server"]
# 커뮤니티 패키지입니다. GitHub: github.com/pab1it0/prometheus-mcp-server
env:
PROMETHEUS_URL: "http://prometheus.internal:9090"설정 후 등록된 도구를 확인하는 방법입니다.
hermes mcp list
# 출력 예시
internal_db:
- query_metrics: Execute SQL query on internal database
- list_tables: List all available tables
- describe_table: Show table schema
grafana:
- list_dashboards: List all Grafana dashboards
- get_dashboard: Get dashboard details
- query_datasource: Execute query against a data source
- list_alerts: List active alerts
prometheus:
- query_range: Execute PromQL range query
- query_instant: Execute instant PromQL query이 구성이 완료되면 Hermes에게 이렇게 물어볼 수 있습니다.
"지난 1시간 동안 API 응답 시간이 500ms를 초과한 서비스 목록을 보여주고,
해당 서비스들의 DB 쿼리 오류 현황도 함께 알려줘"Hermes가 알아서 Prometheus에 PromQL을 실행하고, 결과를 기반으로 내부 DB를 조회한 뒤 통합 분석을 내놓습니다. 처음 이게 실제로 동작하는 걸 봤을 때 꽤 놀랐습니다. 저희 팀에서는 지금도 온콜 상황에 이 구성을 그대로 쓰고 있는데, 장애 원인 파악 시간이 체감상 절반 가까이 줄었습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 표준화된 재사용 | 한 번 만든 MCP 서버를 Hermes, Claude Code, Cursor 등 모든 호환 클라이언트에서 재사용 가능 |
| 보안 경계 분리 | LLM이 DB에 직접 접근하지 않고 MCP 서버가 중간에서 권한과 쿼리를 통제 |
| 실시간 데이터 | 임베딩이나 RAG 없이 DB와 모니터링 시스템에서 최신 데이터를 직접 조회 |
| 도구 필터링 | allowed_tools 설정으로 에이전트 권한을 세밀하게 제어 |
| 언어 독립성 | TypeScript, Python, C# 등 팀이 익숙한 언어로 서버 구현 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 프로덕션 미성숙 | 전체 MCP 서버의 86%가 개발자 로컬 환경에서 동작 중 | 팀 공유 서버는 Streamable HTTP + 게이트웨이 구성 검토 |
| stdio 가시성 부족 | stdio 방식은 감사 로그·중앙 모니터링이 어려움 | 모든 도구 호출을 별도 로깅 미들웨어로 기록 |
| 응답 지연 | 실시간 도구 호출은 RAG 방식보다 레이턴시가 높을 수 있음 | 자주 쓰이는 쿼리 결과 캐싱 고려 |
| OAuth 토큰 관리 | 야간에도 동작하는 에이전트를 위한 토큰 자동 갱신이 복잡 | 서비스 어카운트 토큰이나 장기 유효 API 키 사용 |
실무에서 가장 흔한 실수
-
프로덕션 DB 자격증명을 그대로 쓰는 것. 이 부분에서 한 번 당한 적이 있어서 강하게 말씀드리고 싶은데 — 에이전트가 의도치 않게 장시간 실행되는 쿼리를 날리거나, 예상치 못한 테이블에 접근하는 상황이 생각보다 자주 납니다. 읽기 전용 계정을 별도로 만들어서 MCP 서버에만 할당하는 것, 그냥 넘어가기 쉬운 단계지만 꼭 챙겨야 합니다.
-
allowed_tools를 생략하거나 와일드카드로 두는 것. MCP 서버가 제공하는 모든 도구를 에이전트에게 열어두면, 나중에 관리용 도구가 서버에 추가됐을 때 그것까지 자동으로 접근 가능해집니다. 실제로 필요한 도구만 명시적으로 나열하는 게 훨씬 안전합니다. -
MCP 도구 호출 로그를 남기지 않는 것. 에이전트가 어떤 쿼리를 얼마나 실행했는지 추적이 안 되면 장애 원인 분석이 어렵습니다. 커스텀 MCP 서버라면 도구 호출마다 중앙 로깅 시스템에 기록하는 미들웨어를 추가하면 나중에 크게 도움이 됩니다.
마치며
MCP 서버 하나로 Hermes Agent가 자연어로 사내 DB와 모니터링을 다루게 만드는 것, 생각보다 훨씬 빠르게 시작할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
-
로컬에서 커스텀 MCP 서버 띄워보기 — 위 TypeScript 예시를 복사해서
DB_URL만 개발 DB로 바꾼 뒤pnpm exec tsx src/server.ts로 실행해보시면 됩니다. 서버가 뜨면 stdin으로 아래 초기화 JSON을 보내 MCP 핸드셰이크가 동작하는지 확인할 수 있습니다.json{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}"Connection refused"가 나온다면 DB_URL 오타나 포트 설정이 가장 먼저 확인해볼 지점입니다.
-
Hermes
config.yaml에 등록하고hermes mcp list실행해보기 — 도구 목록이 정상적으로 출력된다면 핸드셰이크가 성공한 것입니다. Hermes가 서버를 못 찾는다면command에 지정한 경로가 절대 경로인지, 실행 권한이 있는지 확인해보세요. 이 단계까지 오면 Hermes가 DB에 자연어로 물어볼 수 있는 상태입니다. -
Grafana 공식 MCP 서버 추가해보기 —
npx @grafana/mcp-grafana한 줄로 설치되고, API 키와 URL만 있으면 바로 대시보드와 알림 데이터를 에이전트가 조회할 수 있습니다. 두 서버를 조합했을 때의 위력을 바로 체감하실 수 있을 겁니다.
지금 이 글을 쓰면서도 옆 탭에는 Hermes가 프로덕션 지표를 뒤지고 있습니다. 새벽 온콜이 여전히 오지만, 적어도 "원인 파악"에 소비하는 시간은 확실히 줄었습니다.
참고 자료
- Model Context Protocol 공식 문서 — Build an MCP Server
- Hermes Agent 공식 문서 — MCP 기능
- Use MCP with Hermes — 공식 가이드
- Hermes Agent MCP Integration 완전 가이드 | Lushbinary
- NousResearch/hermes-agent | GitHub
- Postgres MCP Pro | crystaldba GitHub
- MCP Toolbox for Databases | Google Cloud Blog
- grafana/mcp-grafana | GitHub
- Monitor MCP Servers with Prometheus & Grafana | Grafana Labs
- MCP TypeScript SDK | GitHub
- How to Build a Custom MCP Server with TypeScript | freeCodeCamp
- Chapter 13: MCP Integration — Claude Code vs. Hermes Agent | Ken Huang