VPS에 MCP 서버 구축하기: stdio 및 HTTP 설정 가이드
AI 코딩 에이전트를 위해 VPS에 MCP 서버를 구축하는 방법을 설명합니다. stdio 방식의 로컬 도구 설정부터 TLS와 인증을 적용한 remote HTTP 서버 구축까지, systemd를 활용한 운영 노하우와 JSON-RPC 스트림 관리법을 상세히 다룹니다.
구축 목표
하나의 VPS에 두 가지 MCP 설정을 구축합니다. 첫 번째는 stdio 서버입니다. Claude Code가 자식 프로세스로 실행하며 pipe를 통해 통신하는 filesystem 또는 database 도구입니다. 두 번째는 remote HTTP 서버입니다. systemd 기반의 장기 실행 네트워크 서비스로 동작하며, TLS가 적용된 nginx reverse proxy를 통해 어떤 MCP client에서도 접속할 수 있습니다. 두 설정 모두 설치 과정은 간단합니다. 이 가이드의 핵심은 실제 주의가 필요한 두 가지 요소입니다. JSON-RPC 스트림을 깨끗하게 유지하는 것과, 인증되지 않은 tool endpoint를 공용 인터넷에 노출하지 않는 것입니다.
MCP의 정의
Model Context Protocol은 AI client(Claude Code, Claude Desktop, VPS의 Gemini CLI, 또는 사용자 정의 script)가 외부 tool을 호출하거나 외부 resource를 읽기 위한 표준 방식입니다. model 자체는 아무것도 실행하지 않습니다. model이 client에게 요청하면, client가 MCP server에 JSON-RPC 2.0 메시지를 전달합니다. server는 tool을 실행한 후 결과를 반환합니다. 프로토콜이 하나이므로, 한 번 작성한 server는 MCP를 지원하는 모든 client에서 사용할 수 있습니다.
두 가지 transport 방식이 있으며, 이 가이드는 이를 기준으로 구성됩니다.
- stdio. client가 server를 child process로 생성합니다. 이후 standard input과 standard output을 통해 newline-delimited JSON-RPC 메시지를 주고받습니다. 네트워크, port, auth가 필요하지 않으며, 프로세스 자체가 trust boundary가 됩니다. 대부분의 local tool이 이 방식을 사용합니다.
- Streamable HTTP (및 이전 방식인 HTTP+SSE). server는 지속적으로 실행되는 web service입니다. client는 HTTP를 통해 연결하며, server는 Server-Sent Events를 통해 응답을 스트리밍할 수 있습니다. 이 방식은 하나의 server를 여러 client와 공유하거나, 특정 machine에 영구적으로 상주해야 하는 tool을 실행할 때 사용합니다.
tool이 단일 machine과 단일 user를 위한 것이라면 stdio를 선택하십시오. 공유 service라면 HTTP를 선택하십시오.
Prerequisites and the honest gotchas
root 또는 sudo 권한을 가진 신규 Ubuntu 24.04 KVM VPS를 준비하십시오. 그 외 요구사항은 다음과 같습니다.
- 서버가 작성된 런타임. 대부분의 참조 서버는 Node 또는 Python을 사용합니다. Ubuntu 24.04에는 Node 18이 포함되어 있으나, 최신 MCP 패키지 중 일부는 Node 20 이상을 요구합니다. 따라서
apt를 신뢰하기보다 NodeSource 또는 nvm을 통해 최신 LTS 버전을 설치하십시오. Python 3.12는 이미 설치되어 있습니다. - 도메인 및 DNS A record. 원격 HTTP 서버에만 필요합니다. TLS를 사용하려면 해당 VPS로 연결되는 이름이 필요합니다. stdio 예제는 DNS가 전혀 필요하지 않습니다.
- 512 MB RAM이면 충분합니다. MCP 서버는 가벼운 JSON-RPC 프로세스입니다. 메모리 사용량은 프로토콜이 아니라 도구가 사용하는 요소(데이터베이스 드라이버, 파일 캐시 등)에 따라 결정됩니다.
- 사양(Spec)은 최신 상태이며 변경될 수 있습니다. 2025-03-26 개정판에서 HTTP+SSE가 Streamable HTTP로 대체되었으며 SSE는 deprecated(권장되지 않음)로 표시되었습니다. SSE는 여전히 작동하며 많은 서버가 이를 지원하므로, 특정 전송 방식이 절대적이라고 간주하지 말고 서버의 릴리스 노트를 통해 다시 확인하십시오.
Step 1: Claude Code에 stdio server 연결하기
filesystem server부터 시작하십시오. 이 서버는 공식적으로 제공되며 지속적으로 유지 관리됩니다. Node만 설치되어 있으면 됩니다. 아래의 명령어를 실행하면 Claude Code에 서버가 등록됩니다. 현재 프로젝트로 범위가 제한되어 커밋 가능한 파일로 저장됩니다.
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/api-- 구분자가 중요합니다. 이 구분자 이후의 모든 내용은 Claude Code가 실행할 명령어이며, Claude Code의 플래그가 아닙니다. 이 작업은 프로젝트 루트에 .mcp.json 파일을 생성합니다.
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}아직 실행 중인 것은 없습니다. 이 디렉토리에서 Claude Code를 다시 시작하면, 에이전트가 .mcp.json를 읽고 npx -y @modelcontextprotocol/server-filesystem ...를 자식 프로세스로 생성합니다. 그 후 해당 프로세스의 stdin/stdout을 통해 MCP 핸드셰이크를 수행합니다. 연결 성공 여부를 확인하십시오.
claude mcp list정상적인 서버는 명령어와 함께 녹색 체크 표시(filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected)를 출력합니다. 세션 내부에서 /mcp 슬래시 명령어를 사용하면 서버가 제공하는 도구 목록(read_file, write_file, list_directory)을 확인할 수 있습니다. 이제 에이전트는 허용된 경로에서 해당 도구들을 호출할 수 있습니다. 데이터베이스 도구도 동일한 구조를 가집니다. 패키지를 교체하고 마지막 인자로 연결 문자열을 전달하십시오. 다만, 참조용 Postgres 서버의 패키지 이름이 여러 번 변경되었으므로 서버의 공식 저장소에서 현재 패키지 이름을 확인해야 합니다.
이것이 호스트 머신에서 에이전트를 실행하는 핵심 이유입니다. Claude Code 세션이 tmux를 통해 VPS 내부에서 실행되므로, stdio server는 네트워크 지연 없이 프로젝트 파일 및 로컬 서비스에 직접 접근하며 에이전트 바로 옆에서 실행됩니다.
Step 2: 원격 HTTP server 구축하기
stdio server는 parent process가 종료되면 함께 종료됩니다. 모든 client를 위해 계속 실행되는 도구가 필요하다면 — 예를 들어 공유 ops tool, database gateway, 또는 laptop과 CI가 공통으로 호출하는 도구 등 — HTTP transport와 실제 service가 필요합니다. 다음은 official SDK를 사용하여 하나의 tool을 노출하는 minimal Python server 예시입니다:
# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http")host="127.0.0.1"에 주의하십시오. server는 localhost에만 bind됩니다. 따라서 인증 기능이 구현되기 전까지 외부에서는 직접 접속할 수 없으며, 이는 의도된 동작입니다. systemd가 안정적인 interpreter path를 가질 수 있도록 별도의 virtualenv에 설치하십시오:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Step 3: systemd를 사용하여 프로세스 유지하기
에이전트가 호출할 때 도구가 작동하지 않으면 도구가 없는 것보다 더 나쁩니다. /etc/systemd/system/mcp-ops.service를 작성하십시오:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetExecStart 내 venv Python의 절대 경로는 필수 사항입니다. /usr/bin/python3를 지정하십시오. pip install를 시스템 인터프리터가 인식하지 못하므로 프로세스는 ModuleNotFoundError: No module named 'mcp' 상태로 시작됩니다. 활성화 후 다음을 확인하십시오:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus의 결과는 active (running)이어야 합니다. curl는 본문에 JSON-RPC 오류를 포함하여 HTTP/1.1 400 Bad Request를 반환합니다. 이는 요청에 세션과 유효한 JSON 페이로드가 없음을 의미하며, 이는 의도된 결과입니다. 포트가 응답하고 프로토콜을 사용함을 증명하기 때문입니다. Connection refused 또는 빈 응답이 발생하면 프로세스가 의도한 위치에 바인딩되지 않은 것입니다. journalctl -u mcp-ops -n 50를 확인하십시오.
Step 4: TLS 및 reverse proxy 설정
서버는 localhost에서 대기합니다. 외부에서 접속하려면 nginx에서 TLS를 종료하고 내부로 proxy를 전달해야 합니다. nginx를 설치하고 Certbot 및 Let's Encrypt를 사용하여 nginx에 인증서 설치를 완료한 후, location block을 작성하십시오. 중요한 점은 buffering을 비활성화하는 것입니다. nginx의 기본 동작은 응답이 완료될 때까지 대기하므로, SSE 스트림이 영구적으로 중단될 수 있습니다.
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}sudo nginx -t && sudo systemctl reload nginx로 reload를 수행하십시오. 이미 여러 개의 container를 운영 중이라면 자동 TLS를 지원하는 Traefik reverse proxy를 사용하여 동일한 작업을 수행할 수 있습니다. Traefik은 인증서를 발급하고 hostname을 기준으로 라우팅하며, 사용자는 MCP container에 label만 추가하면 됩니다. 어떤 방식을 사용하든, 이제 reverse proxy만이 public port를 점유하며 아직 보안 설정이 되지 않은 service를 가리키게 됩니다. URL을 어디에 등록하기 전에 이 문제를 먼저 해결하십시오.
Step 5: 이 주제에서 가장 중요한 보안 규칙
인증되지 않은 MCP endpoint를 절대 노출하지 마십시오. MCP server는 읽기 전용 API가 아닙니다. MCP server는 파일, 데이터베이스, 때로는 shell에 대한 tool access 권한을 부여합니다. 공용 인터넷에 /mcp를 개방하는 것은 AI agent와 동일한 권한을 가진 외부인에게 접근을 허용하는 것과 같습니다. 해당 사용자는 tool 목록을 조회한 뒤 이를 실행할 수 있습니다. 인증되지 않은 admin socket과 동일하게 취급하십시오. 실제 성격이 그러하기 때문입니다.
권장 순서에 따른 세 가지 방어 방법입니다:
- 외부에 게시하지 마십시오. server를
127.0.0.1에 유지하고 SSH tunnel을 사용하여 노트북에서 접속하십시오:ssh -L 8000:127.0.0.1:8000 matt@vps을 실행한 후 client가http://127.0.0.1:8000/mcp을 가리키도록 설정하십시오. 이 방식은 그 어떤 것도 외부에 노출되지 않습니다. - 프라이빗 네트워크에 배치하십시오. self-hosted WireGuard VPN의 tunnel address를 바인딩하고 VPN peer만 접속할 수 있도록 설정하십시오. 공용 인터넷에서는 포트가 닫힌 것으로 보입니다.
- 공용 상태여야 한다면 토큰을 요구하십시오. 가장 적절한 방법은 HTTP transport가 기본적으로 지원하는 MCP OAuth flow입니다. 실용적인 최소 요건은 proxy에서 검증하는 shared bearer token입니다. 비용이 저렴하며 무차별적인 공격을 완전히 차단할 수 있습니다:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}openssl rand -hex 32를 사용하여 토큰을 생성하십시오. 서버 앞에 이러한 인증 수단이 없다면 서버 자체를 0.0.0.0에 바인딩하지 마십시오. 그 후 client는 헤더로 토큰을 전송합니다. Claude Code의 경우:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'.mcp.json에 비밀값이 평문으로 저장되지 않도록 shell에 MCP_TOKEN을 설정하십시오. Claude Code는 실행 시점에 환경 변수에서 ${MCP_TOKEN}를 확장하여 읽어옵니다.
Step 6: MCP Inspector를 이용한 디버깅
서버가 정상적으로 동작하지 않을 때, agent 내부에서 추측하지 마십시오. 공식 웹 기반 테스트 클라이언트인 Inspector를 사용하여 직접 제어하십시오. stdio server의 경우, agent가 실행하는 것과 동일한 명령어를 입력하십시오:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmp이 명령은 http://localhost:6274에서 UI를 시작하며, 6277 포트에서 proxy를 실행합니다. (최신 버전은 MCP_PROXY_AUTH_TOKEN 쿼리 스트링이 포함된 URL을 출력합니다. 해당 링크를 정확히 사용해야 하며, 그렇지 않으면 UI 접속이 거부됩니다.) Connect를 클릭한 다음, List Tools를 선택하고, 실제 인자를 사용하여 Call Tool을 실행하십시오. Inspector에서는 정상 작동하지만 agent에서 실패한다면, 버그는 server가 아닌 client 설정에 있습니다. remote HTTP server의 경우, Streamable HTTP transport를 선택하고 https://mcp.example.com/mcp를 입력한 뒤, Authorization 헤더를 추가하고 연결하십시오. 이 방법은 agent를 사용하기 전에 인증과 proxy가 올바른지 확인하는 가장 빠른 방법입니다.
Keeping servers updated
MCP는 빠르게 변화하므로 정기적으로 패치를 적용하십시오. npx -y로 실행된 Node server는 실행될 때마다 최신 버전을 가져옵니다. 이는 편리하지만 재현이 불가능합니다. 테스트를 완료한 정확한 버전을 고정하십시오. npm view @modelcontextprotocol/server-filesystem version에서 버전을 확인한 후 .mcp.json (@modelcontextprotocol/server-filesystem@<version>)의 package name 뒤에 해당 버전을 추가하십시오. 서버 운영이 중요해지면 의도적으로 버전을 업데이트해야 합니다. systemd 환경의 Python server는 sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" 실행 후 sudo systemctl restart mcp-ops를 통해 업데이트합니다. SDK를 업그레이드할 때는 대상 spec revision을 확인하십시오. SSE에서 Streamable-HTTP로 변경될 경우 클라이언트가 요청해야 하는 transport 방식이 변경될 수 있습니다.
Failure modes, with the strings you will see
The agent shows the server failed. claude mcp list prints ✗ Failed to connect이며, TUI에는 MCP server 'filesystem' failed to start가 표시됩니다. claude --debug을 실행하면 보통 Error: spawn npx ENOENT이 나타납니다. 이는 해당 명령어가 agent의 PATH에 없기 때문입니다. 런타임이 누락되었거나 agent가 찾는 경로에 없습니다. Node가 설치되지 않았거나, npx이 없거나, venv Python을 이름만으로 참조하는 경우입니다. 명령어를 절대 경로로 수정하거나 런타임을 설치한 후 다시 연결하십시오.
A stdio server connects, then instantly drops. 클라이언트 로그에 Unexpected token 'S', "Server sta"... is not valid JSON 또는 Failed to parse message과 같은 JSON parse error가 기록됩니다. 원인은 항상 동일합니다. 서버가 stdout에 로그를 기록했기 때문입니다. stdio 모드에서 stdout은 JSON-RPC 채널입니다. 따라서 잘못 출력된 텍스트는 스트림을 손상시키고 핸드셰이크를 중단시킵니다. Node의 경우, console.log이 stdout으로 출력됩니다. console.error을 사용하십시오. Python의 경우, 일반적인 print()은 stdout으로 출력됩니다. sys.stderr으로 설정된 logging을 사용하여 로그를 기록하거나, file=sys.stderr을 전달하십시오. 규칙은 명확합니다. stdio 모드에서는 stdout에는 JSON-RPC만 출력되어야 하며, 사람이 읽는 모든 로그는 stderr로 출력되어야 합니다.
A remote server times out or closes mid-handshake. 클라이언트에서 MCP error -32000: Connection closed 오류가 발생하거나, Inspector가 Connect 상태에서 멈추고 도구 목록을 표시하지 않습니다. nginx를 사용하는 경우 이는 버퍼링 문제입니다. 프록시가 SSE 스트림을 즉시 전달(flush)하지 않고 보관하기 때문에 클라이언트는 응답을 기다리며 대기하게 됩니다. location에 proxy_buffering off;(및 Step 4의 나머지 블록)을 추가하십시오. 공용 URL에 대해 curl -N을 실행하여 확인하십시오. 이벤트 데이터가 마지막에 한꺼번에 나타나는 것이 아니라, 점진적으로 전달되어야 합니다.
Auth is rejected. 클라이언트에 Error POSTing to endpoint (HTTP 401) 또는 401 Unauthorized이 표시됩니다. 헤더가 누락되었거나, 토큰이 잘못되었거나, 클라이언트가 설정을 읽을 때 쉘 변수가 비어 있는 경우입니다. 이는 흔한 실수입니다. 변수가 설정되지 않으면 ${MCP_TOKEN}은 빈 값이 되며, nginx는 값이 없는 Bearer 을 받게 됩니다. 변수를 echo로 확인하고, 헤더를 다시 추가한 뒤, nginx if의 토큰과 정확히 일치하는지 확인하십시오.
The service will not start under systemd. journalctl -u mcp-ops에 ModuleNotFoundError: No module named 'mcp'이 표시됩니다. 이는 ExecStart이 venv 인터프리터 대신 시스템 Python을 가리키고 있음을 의미합니다. 또는 Address already in use이 발생할 수 있습니다. 이는 다른 프로세스가 8000 포트를 사용 중이기 때문입니다. sudo ss -ltnp | grep 8000을 사용하여 해당 프로세스를 찾으십시오.
FAQ
MCP server의 정확한 정의는 무엇입니까?
MCP server는 JSON-RPC 2.0을 사용하여 AI client에 tool 및 resource를 제공하는 프로그램입니다. AI model이 직접 tool을 실행하지는 않습니다. AI model이 client에게 요청하면, client가 MCP server를 호출하고, server가 실행 후 결과를 반환합니다. 표준 프로도콜을 사용하므로 Claude Code, Claude Desktop, Gemini CLI 등 규격을 준수하는 모든 client에서 하나의 server를 사용할 수 있습니다.
stdio와 HTTP transport의 차이점은 무엇입니까?
stdio server는 client에 의해 child process로 실행되며 stdin/stdout을 통해 통신합니다. 따라서 단일 머신의 단일 client와 함께 실행되며 네트워크나 인증이 필요하지 않습니다. HTTP server는 여러 client가 동시에 접속할 수 있는 지속적인 network service이므로 TLS와 인증이 필요합니다. 로컬용 단일 사용자 tool에는 stdio를 사용하십시오. 공유되거나 지속적인 서비스에는 HTTP(현재 server에서는 Streamable HTTP)를 사용하십시오.
원격 MCP server를 어떻게 보안 처리합니까?
MCP server가 파일, database 또는 shell에 대한 access 권한을 가진다고 가정하면, 인증 없이 노출해서는 안 됩니다. localhost에 바인딩한 후 SSH tunnel 또는 private VPN을 통해 접속하는 방식이 가장 안전합니다. 공용으로 사용해야 하는 경우, bearer token 또는 MCP OAuth flow를 강제하는 reverse proxy 뒤에 배치하십시오. openssl rand -hex 32를 사용하여 token을 생성하십시오. 이러한 보안 장치 없이 server를 0.0.0.0에 바인딩하지 마십시오.
실행되지 않는 server를 어떻게 디버깅합니까?
먼저 claude mcp list를 확인하십시오. ✗ Failed to connect와 spawn ... ENOENT 오류는 command 또는 runtime이 누락되었음을 의미하므로, path를 수정하거나 해당 프로그램을 설치하십시오. 연결 후 JSON parse error와 함께 연결이 끊긴다면, server가 stdout으로 로그를 남겨 JSON-RPC stream을 손상시키고 있는 것입니다. 모든 로그를 stderr로 변경하십시오. 그 외의 문제는 MCP Inspector에서 해당 command를 직접 실행하십시오. MCP Inspector는 server를 격리된 상태로 구동하므로, server의 버그인지 client-config의 버그인지 구분할 수 있습니다.