−25%

Windows 연간 결제, 10월 31일까지. 요금제 보기

EQVPS
시작하기

MCP 게이트웨이용 VPS: 여러 MCP 서버를 주소 하나, 토큰 하나 뒤에

팀이 직접 호스팅하는 MCP 서버를 세 개 쓰고, 모두가 URL 세 개와 비밀 세 개를 갖고 있습니다. 작은 VPS의 HTTPS 주소 하나 뒤로 모으세요. Caddy가 bearer 토큰을 확인하고 경로별로 라우팅하며 서버 자체는 인터넷에서 감춥니다. 테스트를 거친 구성입니다.

직접 호스팅하는 MCP 서버는 계속 늘어납니다. 처음엔 사내 문서용 하나, 다음엔 데이터베이스용 하나, 그러다 누군가 배포 파이프라인용으로 작은 걸 하나 씁니다. 저마다 포트, URL, 비밀이 따로 있고, 팀원 모두가 세 가지를 전부 클라이언트 설정에 붙여 넣습니다. 토큰 하나 바꾸려면 모두에게 연락해야 하죠.

게이트웨이는 이것을 주소 하나와 토큰 하나로 만듭니다. 클라이언트는 https://mcp.example.com/docs/mcp, .../db/mcp 같은 주소로 접속합니다. 게이트웨이가 토큰을 확인하고 각 경로를 localhost의 맞는 서버로 보내며, 서버 자체는 절대 외부에 노출되지 않습니다. 필요한 건 아주 적습니다. 전용 IPv4가 있는 작은 VPS와 Caddy면 됩니다.

아래 구성은 테스트 서버에 꾸린 뒤 실제 클라이언트로 확인하고 나서 글로 옮겼습니다.

준비물

  • 클라이언트가 443에 닿을 수 있도록 공인 IPv4가 있는 VPS. MCP 서버 자체가 무거운 일을 하지 않는다면 Nano-IP로 충분합니다.
  • 서버를 가리키는 A 레코드가 있는 도메인. 여기서는 mcp.example.com.
  • Ubuntu 24.04 또는 Debian 12/13. Caddy는 둘 다의 기본 저장소에 있습니다.

1. MCP 서버를 localhost에서 실행하기

각 서버는 systemd 서비스로 돌아갑니다. 여기서는 예시가 독립적으로 돌아가도록 참조 서버 server-everything을 두 번 씁니다. 실제로는 여러분의 서버로 바꾸세요. Node.js 20 이상이 필요합니다.

mkdir -p /opt/mcp && cd /opt/mcp
npm install @modelcontextprotocol/server-everything
# /etc/systemd/system/mcp-everything-1.service
[Unit]
Description=MCP server everything #1
After=network.target

[Service]
WorkingDirectory=/opt/mcp
Environment=PORT=3101
ExecStart=/opt/mcp/node_modules/.bin/mcp-server-everything streamableHttp
Restart=always
User=nobody

[Install]
WantedBy=multi-user.target

두 번째 복사본은 3102번 포트를 씁니다. 그다음:

systemctl daemon-reload
systemctl enable --now mcp-everything-1 mcp-everything-2

테스트 중에 발견한 점: 이 서버는 127.0.0.1만이 아니라 모든 인터페이스에서 대기하고, 다른 서버도 상당수가 그렇습니다. 3단계에서 이를 처리합니다.

2. Caddy: 토큰을 먼저 확인하고, 그다음 경로별로 라우팅

토큰을 만들어 환경 파일로 Caddy에 넘깁니다.

apt install -y caddy
echo "MCP_GATEWAY_TOKEN=$(openssl rand -hex 32)" > /etc/caddy/gateway.env
chmod 600 /etc/caddy/gateway.env
mkdir -p /etc/systemd/system/caddy.service.d
printf '[Service]\nEnvironmentFile=/etc/caddy/gateway.env\n' > /etc/systemd/system/caddy.service.d/env.conf
systemctl daemon-reload

Caddyfile:

mcp.example.com {
	route {
		@unauthorized not header Authorization "Bearer {$MCP_GATEWAY_TOKEN}"
		respond @unauthorized 401

		handle_path /everything/* {
			reverse_proxy 127.0.0.1:3101 {
				flush_interval -1
			}
		}
		handle_path /tools2/* {
			reverse_proxy 127.0.0.1:3102 {
				flush_interval -1
			}
		}
		respond 404
	}
}
caddy validate --config /etc/caddy/Caddyfile && systemctl restart caddy

mcp.example.com 인증서는 Caddy가 알아서 받습니다.

route 블록은 장식이 아닙니다. 첫 버전에서는 토큰 확인과 handle_path 블록을 최상위에 나란히 두었는데, 토큰 없는 요청이 200으로 그냥 통과했습니다. Caddy는 지시어를 자기 방식대로 정렬하고, handle_path가 respond보다 먼저 실행됩니다. route 안에서는 적은 순서대로 실행됩니다. 이 구성을 응용한다면 무엇보다 먼저 "토큰 없음" 경우를 테스트하세요.

handle_path는 접두사를 떼어 내므로 /everything/mcp는 서버에 /mcp로 도착합니다. flush_interval -1은 스트리밍 이벤트를 도착하는 즉시 넘겨줍니다.

3. SSH와 HTTPS를 빼고 모두 닫기

apt install -y ufw
ufw default deny incoming
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw enable

그 뒤 저희 테스트에서 3101, 3102번 포트는 서버 밖에서 닿지 않았고, 22번과 443번은 응답했습니다. 80번 포트는 Caddy가 인증서를 받고 갱신하며 HTTPS로 리디렉션하기 위해서만 열어 둡니다.

4. 확인하기

TOKEN=$(cut -d= -f2 /etc/caddy/gateway.env)
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"1"}}}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/everything/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d "$INIT"

첫 번째 요청은 401, 두 번째는 200이 나와야 합니다. 저희 실행에서는 잘못된 토큰과 잘린 토큰 모두 401을 받았고, 알 수 없는 경로는 올바른 토큰으로도 404를 받았습니다.

그다음 클라이언트에서, 예를 들어 Claude Code로:

claude mcp add --transport http everything https://mcp.example.com/everything/mcp \
  --header "Authorization: Bearer $TOKEN"
claude mcp list

두 서버 모두 ✔ Connected가 표시됐습니다. 같은 클라이언트에 잘못된 토큰을 넣자 ✘ Failed to connect가 표시됐습니다. 클라이언트 설정은 EQVPS MCP를 Claude Code와 Codex에 연결하기에서 더 자세히 다룹니다.

다음 서버 추가하기

새 localhost 포트에서 실행하고, handle_path 블록 하나를 추가하고, Caddy를 다시 불러오세요. 누구의 설정도 바뀌지 않고, 같은 호스트와 같은 토큰 아래 새 URL 하나가 생길 뿐입니다.

알아 둘 한계

  • 모두에게 토큰 하나는 간단하고 교체도 파일 하나면 되지만, 모두의 것을 바꾸지 않고 한 사람만 권한을 회수할 수는 없습니다. 사용자별 접근이 필요하다면 앞단에 인증 프록시를 두거나 사람마다 별도의 경로와 토큰을 주세요.
  • 게이트웨이가 서버를 안전하게 만들지는 않습니다. 셸 명령을 실행하는 MCP 서버는 토큰을 가진 누구에게나 여전히 셸입니다. 토큰을 공유 설정이나 git에 두지 마세요.
  • 크기는 게이트웨이가 아니라 서버에 맞추세요. Caddy 자체는 수십 MB 정도만 씁니다. 서버 중 하나가 로컬 모델을 돌리거나 큰 저장소를 색인한다면 요금제를 정하는 건 프록시가 아니라 그 서버입니다.

MCP 서버가 하나뿐인가요? MCP 서버용 VPS에서 그 더 단순한 경우를 다룹니다. 에이전트가 자기가 사는 서버까지 관리하게 하고 싶다면 EQVPS MCP 서버가 해 줍니다(스크립트를 선호한다면 REST API). 전용 IPv4 요금제는 월 $8부터입니다.

FAQ

MCP 서버를 굳이 게이트웨이 뒤에 둘 이유가 있나요?

서버마다가 아니라 팀마다 주소 하나와 토큰 하나, TLS는 한곳에서 처리, 서버 자체는 localhost에서만 대기합니다. 네 번째 서버를 추가할 때 게이트웨이에서 세 줄만 바꾸면 되고, 모두에게 새 비밀을 나눠 줄 필요가 없습니다.

어떤 요금제가 필요한가요?

전용 IPv4가 있는 요금제입니다. 클라이언트가 80번과 443번 포트에 닿아야 하기 때문입니다. Nano-IP(월 $8, 2 vCPU, 1 GB)면 게이트웨이와 가벼운 MCP 서버 몇 개는 충분합니다. 무거운 일을 직접 하는 서버가 있다면 그 서버에 맞춰 고르세요.

Caddy를 통해 스트리밍이 되나요?

됩니다. Streamable HTTP는 일반 POST 요청과 server-sent events를 씁니다. 리버스 프록시에 flush_interval -1을 설정하면 이벤트가 버퍼에 쌓이지 않고 바로 전달됩니다.

공유 토큰 하나로 충분히 안전한가요?

작은 팀이라면 합리적인 출발점입니다. 토큰은 HTTPS 밖으로 나가지 않고, 파일 하나만 바꾸면 교체됩니다. 사람별 접근이나 권한 회수가 필요해지면 앞단에 제대로 된 인증 계층을 두세요. 공유 토큰은 바닥선이지 천장이 아닙니다.

NAT 요금제에서도 되나요?

공개 게이트웨이로는 안 됩니다. NAT 요금제에는 SSH용 포워딩 포트 하나뿐이고 다른 인바운드가 없어 클라이언트가 443에 닿지 못합니다. 모든 클라이언트가 같은 서버에 있을 때만 쓸 수 있습니다.

댓글

아직 댓글이 없습니다. 첫 번째가 되세요.

댓글 남기기

댓글은 표시되기 전에 검토됩니다.