Skip to main content

3부 12장. Cloudflare Workers에 원격 MCP 서버 배포하기

지금까지 우리는 PKM-MCP를 꽤 성실하게 키워 왔다. 메모를 검색하는 읽기 전용 서버에서 시작해, CRUD 도구를 붙이고, 의미 검색을 더하고, OAuth까지 얹었다. 그런데도 아직 어딘가 반쪽짜리 느낌이 남아 있다. 이유는 간단하다. 서버가 아직도 내 개발 환경의 연장선에 머물러 있기 때문이다.

배포는 단순히 URL 하나를 얻는 일이 아니다. 노트북 안에서만 통하던 도구를, 다른 기기와 다른 사용자와 다른 클라이언트가 만질 수 있는 서비스로 바꾸는 일이다. 이 순간부터 PKM-MCP는 "내가 실험하던 프로젝트"가 아니라 "누군가가 실제로 의존할 수 있는 시스템"이 된다.

이번 장에서는 그 경계를 넘는다. 8장에서 TypeScript로 다시 만든 PKM-MCP를 기준으로, Cloudflare Workers에 원격 MCP 서버를 올리고, 시크릿을 정리하고, mcp.내도메인.com 같은 실제 주소를 붙이고, HTTPS까지 끝낸다. 이 과정을 마치면 PKM-MCP는 더 이상 책 속 예제가 아니라, 진짜 인터넷 서비스가 된다.

3부 12장. Cloudflare Workers에 원격 MCP 서버 배포하기

12.1 왜 Cloudflare Workers가 MCP에 잘 맞는가

원격 MCP 서버는 생각보다 요구사항이 많다. Streamable HTTP를 말끔하게 처리해야 하고, 공개 URL이 있어야 하고, 인증도 자연스럽게 붙어야 한다. 서버를 직접 굴리면 Nginx, 인증 프록시, TLS 인증서, 배포 파이프라인, 확장 전략이 한꺼번에 달려든다. 처음엔 MCP를 배우고 있었는데, 어느새 새벽 두 시에 인증서 갱신 문서를 읽고 있는 자신을 발견하기 쉽다.

Cloudflare Workers가 반가운 이유는, 이 잡일을 꽤 많이 걷어내 주기 때문이다. Cloudflare는 원격 MCP 서버를 위한 공식 가이드와 템플릿을 제공하고, createMcpHandler() 같은 단순한 진입점과 세션 상태가 필요한 경우의 McpAgent, 그리고 OAuth를 쉽게 붙일 수 있는 workers-oauth-provider까지 함께 제공한다. 즉, "MCP 서버를 인터넷에 올리기 위한 바닥 공사"가 이미 어느 정도 되어 있다.12

특히 PKM-MCP처럼 도구 호출 중심의 서버는 Workers의 실행 모델과 잘 맞는다. 요청이 오면 빠르게 처리하고, 외부 저장소나 API를 읽고, 응답을 돌려주는 식의 흐름은 Worker가 잘하는 일이다. 별도 서버를 상시 운영하지 않아도 되고, 첫 배포만으로 *.workers.dev 공개 URL을 얻을 수 있다는 점도 크다.12

여기에 글로벌 엣지 배포라는 장점이 붙는다. AI가 도구를 한 번 부를 때마다 사용자는 짧은 기다림을 체감한다. 검색 한 번, 요약 한 번, 다시 검색 한 번이 쌓이면 왕복 지연은 금방 피로가 된다. Workers는 요청을 사용자 가까운 위치에서 처리하도록 설계되어 있어, 이런 작은 기다림을 줄이는 데 유리하다.2

또 하나 중요한 점은 배포와 인증이 같은 집 안에 있다는 감각이다. 11장에서 우리는 원격 MCP 서버는 OAuth를 우회하지 않는 편이 낫다고 배웠다. Workers에서는 이 흐름을 붙이는 길도 비교적 정돈되어 있다. 즉, 10장에서 배운 Streamable HTTP, 11장에서 배운 OAuth를 12장에서 실제 배포로 이어 붙이기가 좋다.12

물론 마법은 아니다. Workers는 전통적인 리눅스 서버가 아니다. V8 기반 런타임 위에서 돌아가며, Node.js API 전체를 그대로 제공하지 않는다. 파일 시스템에 직접 쓰거나, 장기 실행 프로세스를 붙잡거나, 로컬 디스크를 진실의 원본처럼 사용하는 설계는 금방 벽을 만난다. 예전 로컬 PKM-MCP가 ./data/notes와 SQLite 파일에 기대고 있었다면, 배포판에서는 D1, KV, R2, 외부 PostgreSQL, Pinecone 같은 네트워크 기반 저장소로 옮겨야 한다.6

그래서 이번 장의 전략은 분명하다. Workers에는 8장에서 만든 TypeScript 버전 PKM-MCP를 올리고, 상태는 Worker 밖에 둔다. 이것이 원격 MCP 서버다운 모양새다.

12.2 환경 변수와 시크릿 관리

배포 초반에 가장 많이 하는 실수는 설정값을 코드와 한데 섞는 것이다. 개발할 때는 편하다. 하지만 프로덕션이 되면 금방 위험해진다. GitHub OAuth 클라이언트 시크릿, 벡터 DB API 키, 외부 DB 접속 문자열, 서명 키 같은 값은 코드와 같이 떠돌면 안 된다.

Cloudflare Workers에서는 이 구분이 꽤 명확하다. 민감하지 않은 설정값은 변수로, 민감한 값은 시크릿으로 다룬다. Worker 코드 입장에서는 둘 다 env에서 읽히지만, 저장 방식과 노출 수준이 다르다. 민감한 값은 wrangler secret put으로 넣어 암호화된 시크릿으로 관리하는 편이 맞다.3

로컬 개발도 정리가 잘 되어 있다. Cloudflare는 .dev.vars 또는 .env 파일을 통해 로컬 시크릿을 주입할 수 있다. 중요한 점은 둘을 동시에 섞지 않는 것이다. .dev.vars를 쓰기로 했다면 그 경로로 밀고 가는 편이 덜 헷갈린다. 환경별 파일도 지원하므로, 개발과 스테이징을 나누고 싶다면 .dev.vars.staging 같은 식으로 관리할 수 있다.3

PKM-MCP 기준으로 설정을 나누면 대략 이런 느낌이 좋다.

항목비밀 여부추천 위치
PKM_RESOURCE=https://mcp.example.com/mcp아님vars
APP_ENV=production아님vars
PINECONE_INDEX=pkm-notes아님vars
GITHUB_CLIENT_ID경우에 따라 공개 가능하지만 보통 시크릿처럼 관리secret
GITHUB_CLIENT_SECRET예secret
PINECONE_API_KEY예secret
DATABASE_URL 또는 외부 API 토큰예secret
토큰 암호화/서명용 키예secret

여기서 한 가지 감각을 꼭 잡자. Client ID는 엄밀히 말하면 공개 정보 취급이 가능한 경우도 많지만, 실무에서는 보통 Client Secret과 한 묶음으로 관리하는 편이 덜 헷갈린다. 반대로 PKM_RESOURCE나 APP_ENV 같은 값은 숨길 이유가 없으니 평문 변수로 두면 된다.

Workers 프로젝트가 여러 개로 늘어나면 또 다른 고민이 생긴다. 예를 들어 같은 계정의 여러 Worker가 동일한 시크릿을 함께 써야 할 수 있다. 이럴 때는 개별 Worker 시크릿 말고 Cloudflare Secrets Store 같은 계정 단위 저장소를 고려할 수 있다. 운영팀이 시크릿을 중앙에서 관리하고, 여러 Worker에 바인딩해 재사용하는 패턴에 특히 잘 맞는다.4

실제로 PKM-MCP를 배포할 때는 설정 파일을 이렇게 정리할 수 있다.

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "pkm-mcp",
  "main": "src/index.ts",
  "compatibility_date": "2026-04-26",
  "routes": [
    {
      "pattern": "mcp.example.com",
      "custom_domain": true
    }
  ],
  "vars": {
    "APP_ENV": "production",
    "PKM_RESOURCE": "https://mcp.example.com/mcp",
    "PINECONE_INDEX": "pkm-notes"
  },
  "secrets": [
    "GITHUB_CLIENT_ID",
    "GITHUB_CLIENT_SECRET",
    "PINECONE_API_KEY",
    "SESSION_ENCRYPTION_KEY"
  ]
}

secrets 항목을 선언해 두면, 필요한 시크릿이 실제로 설정되지 않았을 때 배포 과정에서 더 빨리 문제를 발견할 수 있다. 즉, "왜 프로덕션에서만 죽지?" 같은 슬픈 상황을 조금 줄일 수 있다.3

로컬에서는 다음처럼 시작하면 된다.

touch .dev.vars
GITHUB_CLIENT_ID="local-client-id"
GITHUB_CLIENT_SECRET="local-client-secret"
PINECONE_API_KEY="local-pinecone-key"
SESSION_ENCRYPTION_KEY="dev-only-key"

프로덕션 배포 전에는 이렇게 넣는다.

npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put PINECONE_API_KEY
npx wrangler secret put SESSION_ENCRYPTION_KEY

설정 관리가 지루하게 느껴질 수 있다. 하지만 운영에서는 이런 지루함이 곧 안정성이다. 멋진 검색 도구보다 먼저 챙겨야 할 것은, 비밀이 비밀답게 숨겨지는 구조다.

12.3 도메인 연결과 HTTPS

처음 배포하면 보통 pkm-mcp.your-account.workers.dev 같은 주소를 받는다. 개발 단계에서는 훌륭하다. 바로 접속할 수 있고, Inspector로도 테스트하기 쉽다. 하지만 서비스 주소로는 조금 어색하다. 사용자가 기억하기도 어렵고, 무엇보다 우리 서버의 정체성을 드러내는 이름이 아니다.

그래서 프로덕션 단계에서는 전용 서브도메인을 붙이는 편이 좋다. 예를 들면 mcp.example.com 같은 식이다. Cloudflare Workers의 Custom Domain은 이 작업을 꽤 단순하게 만든다. Worker에 도메인이나 서브도메인을 연결하면, Cloudflare가 필요한 DNS 레코드를 만들고 인증서도 자동으로 발급해 준다. 즉, 별도로 인증서를 굽거나 갱신 스크립트를 돌리지 않아도 HTTPS가 기본으로 따라온다.5

여기서 중요한 포인트는 정확한 호스트명 하나를 정하고, 끝까지 일관되게 쓰는 것이다. 11장에서 다뤘던 OAuth의 resource 값, /callback URL, 문서에 적는 접속 주소, 나중에 붙일 Server Card의 엔드포인트까지 모두 같은 기준 주소를 바라봐야 한다. workers.dev로 발급받아 두고, 실제 클라이언트는 mcp.example.com으로 붙고, OAuth 콜백은 또 다른 호스트를 쓰면 꼬이기 시작한다.

특히 PKM-MCP처럼 사용자별 노트를 다루는 서버는 canonical URL이 중요하다. 최종 주소를 https://mcp.example.com/mcp로 정했다면, 최소한 아래 네 가지는 같은 세계관을 가져야 한다.

  • MCP 엔드포인트: https://mcp.example.com/mcp

  • OAuth 콜백: https://mcp.example.com/callback

  • 보호 리소스 식별자: https://mcp.example.com/mcp

  • 문서와 클라이언트 설정 예시의 서버 URL

Custom Domain은 정확한 호스트 이름으로 동작한다는 점도 기억하자. mcp.example.com에 붙였다고 해서 www.mcp.example.com이나 example.com이 자동으로 같은 Worker를 바라보지는 않는다. MCP 서버라면 보통 www보다 전용 서브도메인 하나를 깔끔하게 정하는 편이 좋다.5

정리하면, Workers는 HTTPS를 어렵게 만들지 않는다. 오히려 "이 도메인을 이 Worker에 연결하겠다"만 명확히 하면, 나머지 복잡한 부분을 많이 대신 처리해 준다. 덕분에 우리는 인증서보다 도구 품질에 더 많은 시간을 쓸 수 있다.5

만들기: PKM-MCP를 mcp.내도메인.com으로 배포하기

이제 실제로 올려 보자. 이번 실습은 8장에서 다시 만든 TypeScript 버전 PKM-MCP를 Cloudflare Workers에 배포하는 흐름이다. 핵심은 세 가지다. 로컬 파일 의존을 제거하고, 시크릿을 분리하고, 최종 주소를 고정하는 것.

1) 배포용 프로젝트를 만든다

가장 빠른 출발점은 Cloudflare의 원격 MCP 템플릿이다. 이미 OAuth와 /mcp 진입점이 준비되어 있어서, 우리는 PKM 도구 로직에 집중하기 쉽다.1

npm create cloudflare@latest -- pkm-mcp-worker --template=cloudflare/ai/demos/remote-mcp-github-oauth
cd pkm-mcp-worker
npm install

그다음 Cloudflare에 로그인한다.

npx wrangler login

이 템플릿을 고른 이유는 단순하다. 이번 시점의 PKM-MCP는 이미 사용자 격리를 전제로 움직이기 때문이다. 읽기 전용 공개 서버가 아니라, 로그인 후 각자 자기 노트를 다루는 서비스에 가깝다.

2) 저장소를 Worker 바깥으로 밀어낸다

Workers에서는 로컬 디스크를 믿으면 안 된다. 따라서 기존 PKM-MCP가 파일 시스템이나 로컬 SQLite에 기대고 있었다면, 여기서 한 번 정리해야 한다.6

가장 무난한 선택은 이렇다.

  • 메모 본문과 메타데이터는 D1 또는 외부 PostgreSQL

  • 임베딩 검색은 Pinecone 또는 Chroma Cloud

  • 사용자 식별은 OAuth의 sub

이렇게 바꾸면 search_notes, create_note, update_note, delete_note, semantic_search_notes가 모두 Worker 인스턴스 메모리와 무관하게 동작한다. 즉, 인스턴스가 바뀌어도 같은 저장소를 보고 같은 결과를 만들 수 있다.

3) PKM 도구를 템플릿에 이식한다

템플릿의 예제 도구를 지우고, 8장에서 만든 TypeScript 도구를 붙인다. 구조는 대략 이런 느낌이다.

this.server.tool(
  "search_notes",
  { query: z.string(), limit: z.number().optional() },
  async ({ query, limit }) => {
    const notes = await repo.searchNotes({
      ownerSub: this.props.login,
      query,
      limit: limit ?? 10,
    })

    return {
      content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
    }
  }
)

여기서 중요한 것은 도구 이름보다 ownerSub 같은 사용자 경계다. 11장에서 했던 이야기 그대로, 배포판에서도 모든 검색과 수정은 사용자 기준으로 필터링되어야 한다. 텍스트 검색만 잠그고 벡터 검색 필터를 빼먹으면, 서비스는 배포됐는데 보안은 로컬 감각에 머무는 셈이다.

4) 로컬 OAuth 앱을 만들고 개발 서버를 띄운다

GitHub OAuth를 예로 들면, 로컬 개발용 앱과 프로덕션용 앱을 따로 만드는 편이 안전하다. 로컬 앱의 홈페이지와 콜백은 개발 서버 주소를 기준으로 잡는다.1

  • Homepage URL: http://localhost:8788

  • Authorization callback URL: http://localhost:8788/callback

클라이언트 ID와 시크릿은 .dev.vars에 넣는다. 그리고 서버를 실행한다.

npm start

이제 Inspector를 띄운다.

npx @modelcontextprotocol/inspector@latest

브라우저에서 Inspector를 열고, Worker의 로컬 MCP URL인 http://localhost:8788/mcp에 연결한다. OAuth Quick Flow를 한 번 통과한 뒤 List Tools를 눌렀을 때 PKM 도구들이 보여야 한다.1

이 단계에서 확인할 것은 화려하지 않다. 정말 딱 세 가지다.

  • 로그인 후 도구 목록이 보이는가

  • search_notes가 자기 노트만 찾는가

  • delete_note 같은 쓰기 도구가 권한대로 막히거나 허용되는가

5) 첫 번째 배포를 workers.dev로 끝낸다

로컬이 통과했으면 일단 Cloudflare 기본 도메인으로 올린다.

npx wrangler deploy

배포가 끝나면 보통 이런 주소를 받는다.

https://pkm-mcp-worker.your-account.workers.dev/mcp

이 URL을 Inspector에 그대로 넣어 다시 한 번 연결해 본다. 로컬과 원격이 둘 다 통과하면, 이제 최소한 "인터넷에서 접속 가능한 MCP 서버"는 완성된 것이다.1

이 순간을 가볍게 넘기지 말자. 여기서부터 PKM-MCP는 더 이상 내 노트북이 꺼지면 같이 잠드는 프로그램이 아니다. 이제는 클라우드에서 독립적으로 살아 있는 서버다.

6) 프로덕션용 도메인을 붙인다

이제 workers.dev에서 멈추지 말고, 최종 주소를 붙인다. Cloudflare 대시보드나 wrangler 설정에서 Custom Domain을 추가한다.5

{
  "routes": [
    {
      "pattern": "mcp.example.com",
      "custom_domain": true
    }
  ]
}

다시 배포한다.

npx wrangler deploy

Cloudflare는 이 과정에서 필요한 DNS 레코드와 인증서를 자동으로 준비한다. 별도로 TLS 인증서를 구입하거나 Nginx를 건드릴 필요가 없다.5

이제 최종 MCP URL은 이렇게 된다.

https://mcp.example.com/mcp

7) 프로덕션 OAuth 설정을 최종 주소로 맞춘다

여기서 자주 헷갈린다. 로컬용 OAuth 앱 설정을 그대로 두면 안 된다. 프로덕션 앱은 최종 도메인 기준으로 새로 만든다.1

  • Homepage URL: https://mcp.example.com

  • Authorization callback URL: https://mcp.example.com/callback

그리고 시크릿도 프로덕션 값으로 바꾼다.

npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET

PKM_RESOURCE도 최종 주소에 맞춘다.

{
  "vars": {
    "PKM_RESOURCE": "https://mcp.example.com/mcp"
  }
}

이렇게 해야 11장에서 다뤘던 리소스 식별자, 콜백 URL, 실제 접속 주소가 한 줄로 이어진다. 주소가 셋으로 갈라지면 인증은 대체로 삐걱거린다.

8) 클라이언트에서 실제로 붙여 본다

원격 MCP를 직접 지원하는 클라이언트라면 URL을 그대로 넣으면 된다. 아직 원격 전송이나 인증을 충분히 지원하지 않는 클라이언트라면 mcp-remote 프록시를 사용할 수 있다.12

예를 들어 Claude Desktop 쪽 설정은 이런 식이다.

{
  "mcpServers": {
    "pkm-mcp": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://mcp.example.com/mcp"
      ]
    }
  }
}

이제 Claude에 이렇게 물어보면 된다.

"지난주에 적은 OAuth 메모를 찾아줘."

서버가 제대로 배포되었다면, Claude는 로컬 subprocess가 아니라 인터넷 너머의 PKM-MCP에 붙어서 검색 도구를 호출한다. 그 감각은 꽤 묘하다. 내가 만든 메모 서버가 비로소 "다른 곳에서도 살아 있다"는 느낌이 들기 때문이다.

9) 마지막 스모크 테스트를 한다

배포가 끝났다고 바로 안심하면 안 된다. 최소한 아래 항목은 꼭 확인하자.

  • https://mcp.example.com/mcp에 Inspector가 연결되는가

  • 로그인하지 않았을 때 인증 흐름으로 자연스럽게 이동하는가

  • 로그인 후 자신의 노트만 검색되는가

  • 읽기 권한 토큰으로 삭제 도구가 막히는가

  • workers.dev가 아니라 최종 커스텀 도메인 기준으로 OAuth가 동작하는가

이 다섯 가지가 통과하면, PKM-MCP는 개인 실험을 넘어 배포된 서비스라고 불러도 된다.

정리

Cloudflare Workers가 MCP와 잘 맞는 이유는 단지 유행하는 서버리스 플랫폼이기 때문이 아니다. 원격 MCP 서버가 요구하는 것들, 즉 공개 URL, Streamable HTTP, 인증, 배포 단순성, HTTPS, 전역 접근성을 비교적 한 덩어리로 제공해 주기 때문이다.12

이번 장에서 가장 중요한 감각은 이것이다.

배포는 기능 추가가 아니라 존재 방식의 변화다.

PKM-MCP는 이제 내 컴퓨터에서만 돌아가는 메모 보조 장치가 아니다. 로그인한 사용자가 자기 노트를 검색하고, 수정하고, 의미 검색을 돌리고, 다른 클라이언트에서 같은 서버를 다시 만나는 서비스가 되었다. 그리고 그 변화는 wrangler deploy 한 줄에서 시작되지만, 사실은 저장소 설계, 시크릿 관리, 도메인 일관성, HTTPS, OAuth 정합성까지 전부 한 몸으로 굴러가야 완성된다.

이제 PKM-MCP는 인터넷에 올라왔다. 다음 장에서는 이 살아 있는 서버를 일부러 흔들어 본다. 취약한 서버를 만들어 공격해 보고, 왜 보안은 나중에 덧칠하는 기능이 아니라 처음부터 구조여야 하는지를 직접 확인할 차례다.

참고

1Build a Remote MCP server

2Build and deploy Remote Model Context Protocol (MCP) servers to Cloudflare

3Secrets · Cloudflare Workers docs

4Workers integration · Cloudflare Secrets Store docs

5Custom Domains · Cloudflare Workers docs

6How to: Deploy MCP Server to Cloudflare Workers

책 목록

만들면서 배우는 MCP: PKM-MCP로 끝내는 설계·구현·운영·배포

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기