매니비르 차울라(Manveer Chawla)는 고객이 기존 Zenith 계정으로 Claude 등 MCP 클라이언트에서 SaaS 기능을 이용하도록 Clerk와 Arcade.dev를 연결하는 튜토리얼을 공개했다. 핵심은 사용자 인증과 API 권한 검사를 분리하는 것이다. API가 매 요청마다 사용자의 워크스페이스와 요금제를 확인하므로 모델이 사용자 ID를 주장하거나 다른 테넌트의 데이터에 접근하는 일을 막는다.

전체 예제 저장소에는 Python MCP 서버, 웹 애플리케이션, 설정 템플릿과 테스트가 포함돼 있다. 자체 Clerk 및 Arcade 개발 계정으로 실행할 수 있다.

인증과 권한 확인의 흐름

이 구성은 세 가지 자격 증명을 구분한다. Arcade의 User Source는 고객이 기존 Clerk 계정으로 게이트웨이에 로그인하도록 한다. OAuth 제공자는 도구가 API를 사용자 대신 호출할 수 있도록 위임된 토큰을 발급한다. 브라우저 사용자 검증기는 도구 권한을 승인하는 사람이 해당 흐름을 시작한 애플리케이션 사용자와 같은지 확인한다.

MCP 서버는 Arcade Cloud에서 실행되며, 도구가 호출될 때 Arcade가 해당 사용자의 API 토큰을 제공한다. 백엔드 API는 토큰을 검증하고 애플리케이션의 워크스페이스 멤버십과 요금제 규칙을 적용한다. 모델은 토큰을 받지 않으며, 요청 인자에 사용자 ID를 넣어 권한을 얻을 수도 없다. 고객은 Arcade 계정 없이 기존 애플리케이션 로그인으로 연결할 수 있지만, 서버를 배포하는 개발자는 Arcade 계정이 필요하다.

직접 MCP 서버를 운영하는 방식도 가능하다. Clerk는 동적 클라이언트 등록과 동의 화면을 지원해 MCP 서버의 인증 서버로 사용할 수 있으며, 이 경우 자격 증명 흐름이 단순해진다. 대신 서버를 직접 호스팅하고 운영해야 한다. Clerk의 MCP 헬퍼는 Next.js와 Hono 같은 JavaScript 프레임워크를 대상으로 하며, 이 튜토리얼의 도구는 Python으로 작성됐다.

Arcade 게이트웨이는 여러 MCP 서버를 하나의 주소 뒤에 연결하고 도구 접근 정책을 적용하는 확장 지점으로 소개된다. 다만 로컬 HTTP 모드는 검증된 호출자 신원이 없어 requires_auth 또는 requires_secrets를 선언한 도구를 실행하지 못한다. 튜토리얼은 Arcade Cloud 배포를 사용하며, 보호된 도구는 설정된 개발자 자격 증명을 사용하는 로컬 stdio 방식으로도 실행할 수 있다.

구성과 실행

예제를 실행하려면 Python 3.11–3.13, uv, Node.js 22–24, npm이 필요하다. Python 3.12와 Node.js 22가 예시로 권장된다. Clerk 개발 인스턴스, 별도의 Arcade 데모 프로젝트, 원격 연결을 지원하는 MCP 클라이언트도 준비한다. 로컬 데모에서는 Arcade Cloud가 접근할 수 있도록 ngrok HTTPS 터널이 필요하다. 이미 HTTPS로 배포한 API와 브라우저 검증기 주소가 있다면 터널은 생략할 수 있다.

저장소의 mcp/에는 배포할 Python 서버가, web/에는 로그인·사용자 검증기·보고서 API가 들어 있다. 예제는 Clerk에서 Alice, Bob, Carol 테스트 계정을 만들고 사용자 ID를 애플리케이션 설정에 넣도록 안내한다. Alice와 Bob은 Pro 요금제의 team-red를 공유하고, Carol은 Basic 요금제의 team-blue에 속한다. 이 계정과 권한은 예제 코드의 고정 데이터이며 Clerk Organizations나 결제 설정은 필요하지 않다. 선택 사항으로 두 워크스페이스에 접근하는 내부 직원 계정을 설정할 수도 있다.

Clerk에서는 같은 인스턴스에 두 개의 OAuth 애플리케이션을 만든다. 하나는 보고서 API 접근용이고, 다른 하나는 게이트웨이 로그인용이다. 두 앱은 각자 다른 클라이언트 자격 증명과 콜백을 사용한다. 보고서 API 앱은 토큰 갱신을 위해 offline_access를 요청한다. Clerk의 개발 인스턴스 공개 키와 백엔드 비밀 키도 별도로 준비해야 한다.

Arcade 프로젝트에서는 API용 Custom OAuth Provider를 설정하고 생성된 리디렉션 URI를 보고서 API OAuth 앱의 허용 주소에 등록한다. 이어서 브라우저 사용자 검증기 주소를 https://YOUR-NGROK-DOMAIN/auth/verify 형식으로 등록한다. 검증기는 Clerk 브라우저 세션에서 확인한 사용자 ID를 Arcade에 전달해 흐름을 확인한다. 사용자 ID를 MCP 인자나 브라우저 쿼리에서 받지 않으며, Arcade API 키는 서버에 둔다. 게이트웨이 로그인에는 별도로 Clerk User Source를 구성하고 해당 OAuth 앱의 리디렉션 주소를 등록한다.

웹 앱을 실행할 때 APP_URL에는 ngrok의 HTTPS 주소를 넣고, Clerk 키와 테스트 사용자 ID, API OAuth 클라이언트 ID를 설정한다. ngrok 주소를 바꾸면 APP_URL, Arcade 검증기 주소, 서버의 API 기본 주소도 갱신해야 한다. Arcade Cloud에서 실행되는 서버는 개발자 컴퓨터의 localhost에 접근할 수 없다.

MCP 서버는 Arcade에 배포한 뒤 서버 설정에 REPORT_API_BASE_URL을 공개 HTTPS API 주소로 추가한다. 게이트웨이를 만들고 배포한 네 가지 보고 도구를 선택한 다음, Non-Arcade Users 설정에서 Clerk User Source를 연결한다. Claude Desktop에서는 해당 게이트웨이 URL을 사용자 지정 커넥터로 등록하고 Alice 계정으로 로그인한다. 첫 도구 호출에서 API 동의와 브라우저 검증을 진행할 수 있다. get_current_user 결과에 Alice와 team-red가 나타나는지 확인하면 로그인, 도구 승인, API 접근을 함께 점검할 수 있다.

보고서 작업과 권한 테스트

예제는 장시간 실행되는 작업을 start_report, 상태 확인, 결과 조회의 흐름으로 다룬다. 보고서 시작 요청은 HTTP 202와 report_id, 상태, 다음 확인까지 기다릴 시간을 반환한다. 작업이 완료될 때까지 기다렸다가 결과를 가져오며, 실패하면 중단하고 대기 상태 확인은 최대 횟수를 정하도록 안내한다. 각 호출에서 워크스페이스 멤버십과 요금제를 다시 확인한다. 따라서 보고서 ID를 알아도 결과 접근 권한이 생기지 않는다.

데모의 작업 저장소는 메모리 기반이며 보고서는 15분 뒤 만료된다. 웹 프로세스를 재시작하면 작업 상태가 사라진다. 실제 서비스에서는 기존의 내구성 있는 작업 시스템을 사용해야 한다. POST 요청이 시간 초과되면 보고서가 이미 생성됐을 수 있으므로 무조건 다시 제출해서는 안 된다. 운영 환경에서는 중복 요청에 안전하도록 작업 생성의 멱등성도 고려해야 한다.

테스트에서는 Alice와 Bob이 같은 워크스페이스의 보고서를 공유하고, Carol은 다른 고객의 데이터에 접근하지 못하는지 확인한다. Basic 요금제의 Carol은 고급 보고서를 만들 수 없어야 한다. 직원 계정을 설정했다면 두 워크스페이스에 접근하되 각 요금제 제한은 유지되는지도 시험한다. 보고서 상태와 결과 조회에서도 권한을 검사해야 하며, 접근할 수 없는 보고서 ID는 알 수 없는 ID와 같은 응답을 돌려주도록 예제가 설명한다.

제공된 테스트는 합성 권한 데이터와 요청 동작을 확인한다. 로컬 인사말 테스트나 서버 상태 확인만으로는 인증 흐름과 백엔드 접근이 검증되지 않는다. MCP 서버와 웹 애플리케이션 로그는 따로 살피고, 토큰이나 고객 데이터를 포함할 수 있는 응답 본문을 기록하지 말아야 한다. 운영 적용 시에는 고정된 예제 멤버십 대신 실제 권한 규칙을 연결하고, 멤버십 변경·요금제 하향·진행 중인 작업의 처리 방식을 정해야 한다.

MCP 클라이언트는 Streamable HTTP 전송을 지원하면 같은 게이트웨이 URL을 사용할 수 있다. 튜토리얼은 Claude Desktop으로 연결하며, ChatGPT는 사용자 지정 커넥터를 지원하는 요금제와 개발자 모드가 필요하다고 설명한다. Arcade 게이트웨이 구성 안내와 Claude 연결 안내에서 관련 절차를 확인할 수 있다.

부록: 원문 예시

text
from typing import Annotated from arcade_mcp_server import MCPApp app = MCPApp(name="hello_demo", version="1.0.0") @app.tool def greet(name: Annotated[str, "Name to include in the greeting."]) -> str:     """Return a greeting to verify that a client can call this local tool."""     return f"Hello, {name}!" if __name__ == "__main__":     app.run(transport="stdio")
text
@app.tool(requires_auth=REPORT_AUTH, requires_secrets=[API_CONFIG_KEY]) async def get_current_user(context: Context) -> dict:     """Get the API-verified user and their permitted workspaces.     Call this to discover the workspace IDs available to the signed-in user.     The API derives identity from the OAuth token; the model supplies no user ID.     """     return await _api_request(context, "GET", "/api/me", expected_statuses={200})