[세법MCP] 한국 세법 자료 통합 MCP 원격 공개 배포

로컬 MCP를 원격 HTTP로 전환

처음에는 STDIO 방식으로만 사용했다. 클라이언트가 로컬에서 서버 프로세스를 직접 실행하므로 사용하는 컴퓨터마다 설치와 등록이 필요했다. 원격 HTTP로 바꾸면 클라이언트에는 URL만 등록하면 된다.

배포 대상은 zisu17/korean-taxlaw-mcp다. 국세법령정보시스템과 지방세 법령정보시스템의 원문을 조회해 구조화하며, 검색과 상세 조회는 비동기 HTTP 요청으로 처리한다. 로컬 실행과 달리 원격 배포에서는 여러 사용자의 요청이 Cloud Run 인스턴스에서 같은 업스트림으로 나간다.

실제 배포 구성

이 글은 실제로 배포한 구성을 기준으로 한다. Cloud Run 서비스명은 korean-taxlaw-mcp, 리전은 asia-northeast3, Firebase Hosting 사이트는 korean-taxlaw를 사용했다. GCP 프로젝트 ID와 프로젝트 번호는 명령에서 현재 프로젝트 값을 읽도록 처리했다.

1.1 원격 전환 후 달라지는 점

  • 클라이언트 설치 부담 감소 : URL만 등록하면 되므로 각 기기에서 Python과 패키지를 맞출 필요가 없다.
  • stateless 전제 : 요청 사이에 같은 인스턴스가 유지된다고 가정하지 않는다.
  • 업스트림 요청 집중 : 여러 사용자의 요청이 Cloud Run 쪽에서 같은 원본 시스템으로 나간다.
  • 클라우드 비용 발생 : Cloud Run 실행 시간뿐 아니라 빌드 이미지와 전송량도 확인해야 한다.
  • 공개 엔드포인트 관리 : 인증 없이 열면 호출자별 제어를 별도로 구현해야 한다.

Cloud Run을 선택한 이유

호스팅 후보로 Prefect Horizon, Vercel, Cloud Run을 검토했다. 이 서버에서는 비용보다 최대 인스턴스 수를 직접 제한할 수 있는지를 먼저 봤다.

2.1 왜 인스턴스 상한이 기준인가

서버에는 업스트림 요청을 제한하는 토큰버킷이 있고 이 값은 프로세스 단위로 동작한다. 예를 들어 인스턴스마다 분당 30회로 제한해도 인스턴스가 20개까지 늘어나면 전체 요청은 분당 최대 600회가 될 수 있다. 그래서 현재 운영에서는 Cloud Run의 최대 인스턴스를 1개로 고정했다.

2.2 후보 비교

기준 Prefect Horizon (무료) Vercel (Hobby) Cloud Run
인스턴스 상한 설정 불가 설정 불가. 최대 3만 동시성까지 자동 확장 --max-instances 1
공개 범위 OAuth 필수, 무료는 개발자 1인. 멀티유저 접근 제외 무인증 공개 가능 --allow-unauthenticated
상업적 사용 개인 프로젝트 한정 비상업적·개인용만 (공정사용 지침) 제한 없음
리전 선택 불가 서울(icn1) 지정 가능 서울(asia-northeast3)
로그 보존 1시간 1시간 Cloud Logging 기본 보존
배포 단위 GitHub 저장소 + 진입점 ASGI 앱 객체 컨테이너 이미지

2.3 선택

  • Horizon : 배포는 간단하지만 무료 등급 조건이 공개 서비스 용도와 맞지 않았다.
  • Vercel : 공개 HTTP 엔드포인트는 만들 수 있지만 현재 서버의 프로세스 단위 rate limit 구조와 맞지 않았다.
  • Cloud Run : 최대 인스턴스, 리전, 공개 여부를 배포 설정에서 정할 수 있고 요청이 없을 때 0개까지 축소할 수 있어 현재 구조에 가장 잘 맞았다.

요청 기반 과금 기준 무료 구간은 월 200만 요청, 18만 vCPU-초, 36만 GiB-초다. 트래픽이 많지 않은 MCP라면 이 범위 안에 머물 가능성이 높지만, 무료 구간만 믿기보다 최대 인스턴스와 Spend cap을 함께 설정하는 편이 안전하다.

원격 실행 준비

3.1 원격 호스팅용 진입점

저장소에는 원격 호스팅용 main.py가 있다. src/ 디렉터리를 Python 경로에 추가한 뒤 korean_taxlaw_mcp.server의 mcp 객체를 가져온다. 파일 경로로 진입점을 읽는 플랫폼에서도 패키지의 상대 임포트가 깨지지 않도록 둔 코드다.

# main.py: 원격 호스팅 진입점
import pathlib
import sys

_SRC = pathlib.Path(__file__).resolve().parent / "src"
if str(_SRC) not in sys.path:
    sys.path.insert(0, str(_SRC))

from korean_taxlaw_mcp.server import mcp

Cloud Run 컨테이너에서는 프로젝트의 콘솔 스크립트인 korean-taxlaw-mcp를 직접 실행했다. main.py는 다른 원격 호스팅 방식도 사용할 수 있도록 저장소에 그대로 두었다.

3.2 stateless HTTP

원격 배포에서는 요청 사이에 인스턴스가 사라질 수 있고, 앞단에 프록시가 붙을 수도 있다. 세션을 붙잡는 기본 동작 대신 상태 없는 모드로 띄우면 세션 고정 요구가 사라진다. FastMCP는 환경변수로 켤 수 있다.

환경변수 값 효과
FASTMCP_STATELESS_HTTP true 요청마다 새 세션. 세션 ID 없이 tools/list 가능
FASTMCP_JSON_RESPONSE true SSE 스트림 대신 JSON 단건 응답. CDN·프록시 경유에 유리
FASTMCP_STREAMABLE_HTTP_PATH / 엔드포인트를 /mcp에서 루트로 이동

3.3 컨테이너

Cloud Run 배포용 컨테이너에서는 의존성 레이어와 소스 레이어를 나눴다. 코드만 수정한 경우 패키지 설치 레이어를 재사용할 수 있다. Cloud Run이 PORT 환경변수를 넣어 주므로 시작 명령에서 해당 값을 읽어 서버를 실행한다.

FROM python:3.13-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy \
    PYTHONUNBUFFERED=1 \
    PATH="/app/.venv/bin:$PATH"

WORKDIR /app

# 의존성 레이어를 소스와 분리. README는 pyproject 의 readme 가 참조한다
COPY pyproject.toml uv.lock README.md ./
RUN uv sync --locked --no-dev --no-install-project

COPY src ./src
RUN uv sync --locked --no-dev

ENV FASTMCP_STATELESS_HTTP=true

CMD ["sh", "-c", "korean-taxlaw-mcp --http --host 0.0.0.0 --port ${PORT:-8080}"]

3.4 배포 전 로컬 검증

배포 전에는 도구 목록뿐 아니라 실제 세법 자료 조회까지 확인했다. MCP 프로토콜 응답이 정상이어도 업스트림 사이트 요청이 실패할 수 있기 때문이다.

docker build -t taxlaw-mcp:test .
docker run --rm -p 18080:8080 -e PORT=8080 taxlaw-mcp:test

# 도구 목록
curl -sS -X POST http://localhost:18080/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# 실제 조회: 응답 결과까지 확인
curl -sS -X POST http://localhost:18080/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_tax_interpretations","arguments":{"query":"공동상속주택"}}}'

Cloud Run 배포

4.1 소스에서 바로 배포

배포 작업 디렉터리에 Cloud Run용 Dockerfile을 준비하고 --source .로 배포했다. Cloud Build가 컨테이너 이미지를 만들고 Cloud Run 리비전까지 생성한다.

gcloud run deploy korean-taxlaw-mcp \
  --source . \
  --region asia-northeast3 \
  --allow-unauthenticated \
  --max-instances 1 \
  --concurrency 40 \
  --memory 512Mi \
  --timeout 60 \
  --set-env-vars "^@^TAXLAW_RATE_PER_MIN=30@TAXLAW_TIMEOUT_MS=12000@TAXLAW_RETRIES=1"

^@^는 값에 공백이나 쉼표가 들어갈 때 구분자를 바꾸는 gcloud 문법이다.

4.2 플래그별 근거

플래그 근거
--allow-unauthenticated 인증 없이 URL을 호출할 수 있도록 공개. 제거하면 IAM 인증이 필요함
--max-instances 1 요청 한도가 프로세스 전역 토큰버킷이므로, 인스턴스가 늘면 업스트림 요청량이 곱해짐
--concurrency 40 HTTP 조회가 대부분 비동기 I/O라 한 인스턴스에서 여러 요청을 함께 처리
--timeout 60 기본 300초. 업스트림이 멈추면 한 요청이 5분간 CPU를 점유
TAXLAW_TIMEOUT_MS·TAXLAW_RETRIES 12초 × 2회로 눌러 요청 타임아웃 안에서 끝나게
TAXLAW_RATE_PER_MIN 공개 운영에서는 기본값보다 낮춰 상대 시스템 부담을 줄임

4.3 새 프로젝트의 IAM 전제

Cloud Build 기본 서비스 계정 정책이 바뀐 뒤, 새 프로젝트에서는 컴퓨트 기본 서비스 계정에 빌드 권한이 자동으로 붙지 않는다. 소스 업로드 단계에서 PERMISSION_DENIED가 나면 원인은 코드가 아니라 이것이다.

PROJECT_ID=$(gcloud config get-value project)
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role=roles/cloudbuild.builds.builder

Cloud Run 소스 배포에 대해 구글이 권장하는 역할은 roles/run.builder다. 둘을 함께 부여해 두면 경로가 어느 쪽이든 걸리지 않는다. 권한 전파에 30초~1분이 걸린다.

4.4 배포 확인

MCP 엔드포인트는 JSON-RPC 요청을 POST로 받는다. 브라우저 주소창의 GET 요청으로는 정상 동작 여부를 확인하기 어렵기 때문에 curl로 직접 요청했다.

URL=$(gcloud run services describe korean-taxlaw-mcp \
        --region asia-northeast3 --format 'value(status.url)')

curl -sS -X POST "$URL/mcp" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  -o /tmp/mcp.json -w 'HTTP %{http_code}\n'

head로 파이프를 끊으면 curl: (23)이 뜬다. 응답 자체와 무관한 잡음이라 파일로 받아 확인하는 편이 낫다.

Firebase Hosting으로 주소 정리

Cloud Run이 발급한 기본 주소는 그대로 사용할 수 있지만, 클라이언트에 등록할 주소를 짧게 만들기 위해 경로와 호스트를 정리했다.

5.1 경로 제거

환경변수 하나로 엔드포인트를 루트로 옮긴다. 재빌드 없이 서비스 업데이트만으로 적용된다.

gcloud run services update korean-taxlaw-mcp \
  --region asia-northeast3 \
  --update-env-vars FASTMCP_STREAMABLE_HTTP_PATH=/

바꾼 뒤 /mcp로 요청하면 본문이 빈 404가 돌아온다. 경로를 옮겼으면 검증 요청도 루트로 보내야 한다.

5.2 호스트 구성 비교

방법 결과 판단
Cloud Run 도메인 매핑 서울 리전에서 사용하지 않음 현재 구성에서는 제외
글로벌 외부 ALB 도메인 자유 전달 규칙 고정비가 월 단위로 발생
Firebase Hosting rewrite korean-taxlaw.web.app Cloud Run 앞단 주소로 사용
Firebase + 커스텀 도메인 mcp.도메인 도메인 보유 시 추가 비용 없이 연결

5.3 Firebase Hosting 연결

같은 GCP 프로젝트에 Firebase Hosting을 연결하고 모든 요청을 korean-taxlaw-mcp 서비스로 rewrite했다. Cloud Run의 run.app 주소는 그대로 유지된다.

{
  "hosting": {
    "site": "korean-taxlaw",
    "public": "public",
    "rewrites": [
      {
        "source": "**",
        "run": { "serviceId": "korean-taxlaw-mcp", "region": "asia-northeast3" }
      }
    ]
  }
}
  • 사이트 생성과 rewrite 배포는 별개 : 사이트를 만든 뒤 Hosting 설정을 실제로 배포해야 Cloud Run으로 연결된다.
  • Blaze 요금제 필요 : Cloud Run rewrite를 사용하려면 결제 계정 연결이 필요하다.
  • 요청 타임아웃 : Cloud Run과 애플리케이션의 업스트림 타임아웃을 함께 맞춘다.
  • 응답 형식 : 현재 구성에서는 FASTMCP_JSON_RESPONSE=true 로 JSON 단건 응답을 사용한다.

최종 MCP 주소는 https://korean-taxlaw.web.app 형태로 정리했다. 클라이언트에는 이 주소만 등록하면 된다.

비용 상한과 과금 관리

Cloud Run은 Spend cap을 사용할 수 있다

2026년 7월 27일부터 Google Cloud Billing의 Spend cap budgets가 Preview로 제공되고 Cloud Run도 대상 서비스에 포함됐다. 지정한 월 예산을 넘으면 해당 프로젝트의 Cloud Run 새 요청이 자동으로 중지된다. 기존 Budget alert와 달리 실제 차단 동작이 있다.

6.1 Spend cap과 Cloud Run 설정을 같이 사용

Spend cap이 생겼다고 해서 --max-instances 1을 뺄 이유는 없다. 둘의 역할이 다르다. 최대 인스턴스는 순간적으로 사용량이 커지는 것을 막고, Spend cap은 월 누적 비용이 정한 금액에 도달했을 때 Cloud Run 사용 자체를 멈춘다.

  • 최대 인스턴스 1개 : 프로세스 단위 rate limit이 여러 인스턴스로 나뉘지 않게 하고 Cloud Run 사용량 증가 폭도 제한한다.
  • 요청 타임아웃 60초 : 업스트림 응답이 늦어져 요청이 오래 남는 상황을 줄인다.
  • 동시성 40 : 인스턴스를 늘리지 않고 비동기 HTTP 요청을 처리하기 위한 값이다.
  • Spend cap : 월 누적 Cloud Run 비용이 설정 금액에 도달하면 새 사용을 중지한다.

6.2 Spend cap 설정

Spend cap은 현재 Google Cloud Console의 Cloud Billing에서 설정한다. 일반 예산 알림을 Spend cap으로 바꾸는 방식이 아니라 새 예산을 만들면서 Spend cap enforcement를 선택해야 한다.

  1. Cloud Billing > Budgets & alerts 에서 새 예산을 생성한다.
  2. Spend cap enforcement 를 선택한다.
  3. 이 MCP가 배포된 GCP 프로젝트 하나와 서비스 Cloud Run 을 선택한다.
  4. 월 목표 금액을 입력하고 저장한다.

이 기능은 프로젝트 안의 특정 Cloud Run 서비스 한 개만 막는 설정이 아니다. 선택한 프로젝트의 Cloud Run 서비스 전체에 적용된다. MCP만 따로 운영하는 프로젝트라면 오히려 관리하기 편하다.

Spend cap의 범위

  • Spend cap은 추정 비용을 기준으로 빠르게 동작하지만 차단이 즉시 적용되는 것은 아니다. 처리 지연 동안 발생한 초과 비용은 청구될 수 있다.
  • 차단 시 이미 진행 중인 요청은 완료될 수 있다.
  • Cloud Run만 선택했다면 Artifact Registry, Firebase Hosting 같은 다른 서비스 비용까지 막아 주지는 않는다.
  • 현재 기능은 Preview이고 차단에도 지연이 있을 수 있다. 월 지출 상한을 7,000원으로 잡는다면 Spend cap은 그보다 조금 낮게 두는 편이 안전하다.

Google Cloud 문서: Manage spend cap budgets

6.3 기존 예산 알림은 보조로 유지

기존 Budget alert는 비용 추이를 확인하는 용도로 그대로 둘 수 있다. Spend cap과 역할이 겹치지 않는다. 아래처럼 7,000원 예산을 두고 50%, 90%, 100% 구간에서 알림을 받도록 설정했다.

PROJECT_ID=$(gcloud config get-value project)

gcloud services enable billingbudgets.googleapis.com

gcloud billing budgets create \
  --billing-account=BILLING_ACCOUNT_ID \
  --display-name="korean-taxlaw-mcp" \
  --budget-amount=7000KRW \
  --filter-projects="projects/${PROJECT_ID}" \
  --threshold-rule=percent=0.5 \
  --threshold-rule=percent=0.9 \
  --threshold-rule=percent=1.0 \
  --threshold-rule=percent=0.9,basis=forecasted-spend

6.4 Cloud Run 밖의 비용도 따로 확인

Cloud Run Spend cap은 Cloud Run 비용만 대상으로 한다. 소스 배포를 반복하면 Artifact Registry에 이미지가 쌓이므로 저장 용량은 별도로 정리한다.

항목 관리 방법 현재 구성에서 확인할 점
Cloud Run Spend cap + --max-instances 1 월 200만 요청, 18만 vCPU-초, 36만 GiB-초 무료 구간
Firebase Hosting 전송량 확인 MCP JSON 응답 크기와 호출량에 따라 증가
Cloud Build 배포 횟수 확인 소스 배포 시 빌드 사용량 발생
Artifact Registry cleanup policy Cloud Run Spend cap 대상이 아니므로 이미지 누적 관리 필요

Artifact Registry에는 최근 이미지만 남도록 cleanup policy를 설정했다. 처음에는 --dry-run으로 삭제 대상을 확인한 뒤 적용하는 편이 안전하다.

cat > policy.json <<'JSON'
[
  {"name": "keep-recent", "action": {"type": "Keep"},
   "mostRecentVersions": {"keepCount": 2}},
  {"name": "delete-old", "action": {"type": "Delete"},
   "condition": {"olderThan": "14d"}}
]
JSON

gcloud artifacts repositories set-cleanup-policies cloud-run-source-deploy \
  --location=asia-northeast3 --policy=policy.json --dry-run

공개 운영 시 확인할 점

원격 공개 후에도 운영 측면에서 따로 관리해야 할 부분이 있다.

7.1 호출자별 한도가 없다

현재 rate limit은 프로세스 전체에 적용된다. 한 사용자가 짧은 시간에 많은 도구를 호출하면 다른 사용자도 429를 받을 수 있다. 호출자별 제한이 필요해지면 애플리케이션에서 IP 또는 API 키 기준 제한을 추가하거나 앞단 게이트웨이에서 제어해야 한다.

7.2 데이터센터 IP

국세와 지방세 원본 시스템은 서로 다른 사이트이므로 Cloud Run에서 각각 실제 조회를 확인해야 한다. 로컬에서 정상이어도 데이터센터 IP에서 응답 정책이 다를 수 있기 때문이다.

7.3 User-Agent의 정직성

공개 서비스에서 업스트림으로 요청을 보낼 때는 브라우저처럼 보이게 꾸미기보다 실제 서비스 이름과 저장소 주소를 User-Agent에 넣는 편이 낫다.

TAXLAW_USER_AGENT="korean-taxlaw-mcp/1.0 (+https://github.com/zisu17/korean-taxlaw-mcp)"

7.4 캐시와 한도의 인스턴스 지역성

메모리 캐시와 토큰버킷은 인스턴스 내부에만 존재한다. 현재는 최대 인스턴스를 1개로 두었기 때문에 문제가 없다. 나중에 인스턴스 수를 늘리면 Redis 같은 공유 저장소나 분산 rate limit 구조를 다시 검토해야 한다.

7.5 무인증 공개의 범위

  • 조회 전용 : 이 서버는 공개 자료를 읽기만 한다. 쓰기 기능이 추가되면 인증 정책을 다시 잡아야 한다.
  • 인증이 필요해지면 : Cloud Run IAM으로 닫거나 별도 게이트웨이를 둘 수 있다.
  • 업스트림 보호 : 공개 운영자가 rate limit과 원본 시스템 이용 조건을 계속 관리해야 한다.

구성 정리

  • zisu17/korean-taxlaw-mcp 를 원격 HTTP로 공개하면서 Cloud Run 서울 리전을 사용했다.
  • 프로세스 단위 rate limit을 유지하기 위해 Cloud Run 최대 인스턴스를 1개로 제한했다.
  • 원격 호스팅용 main.py 와 컨테이너 실행 경로를 분리해 로컬 STDIO 사용도 그대로 유지했다.
  • Firebase Hosting rewrite를 사용해 클라이언트에 등록할 주소를 korean-taxlaw.web.app 로 정리했다.
  • 2026년 7월 공개된 Spend cap budgets를 사용하면 Cloud Run 월 비용이 정한 금액에 도달했을 때 새 요청을 자동으로 중지할 수 있다.
  • Spend cap은 Cloud Run만 대상으로 하므로 Artifact Registry와 Firebase Hosting 비용은 별도로 관리해야 한다.
  • 공개 운영에서는 호출자별 rate limit, 업스트림의 클라우드 IP 정책, User-Agent를 계속 확인해야 한다.

Categories:

Updated:

Leave a comment