Trawling

데이터베이스를 한 번의 클릭으로 MCP 서버로 만들기

ZenStack Studio가 기존 PostgreSQL·MySQL·SQLite 데이터베이스를 introspect해 MCP 서버로 연결하는 과정을 소개합니다. Code Mode의 타입 검사와 모델별 권한, 사용자별 access policy를 결합해 복잡한 구축 절차를 한 번의 설정으로 줄이고, 요청마다 사용자 identity를 전달하는 stateless 구조를 구현합니다.

본문

Kent Beck의 조언으로 알려진 “Make it work, make it right, make it fast”를 바탕으로, 이 글은 데이터베이스용 MCP(Model Context Protocol) 서버를 구축해 온 시리즈의 세 번째 단계를 설명합니다. 저자가 말하는 이번 단계는 밀리초 단위의 성능을 뜻하는 “fast”가 아니라 “make it easy”에 가깝습니다. 첫 번째 글에서는 OAuth 인증을 포함한 원격 MCP 서버를 처음부터 만들고, 모든 모델의 CRUD 작업을 각각의 도구로 노출했습니다. 다만 스키마가 작을 때만 실용적이었습니다. 두 번째 글에서는 단일 도구의 설명만으로 컨텍스트 윈도가 400K 토큰까지 커지는 문제를 해결하기 위해 전체 기능을 schema, check, execute 세 도구로 축약하는 Code Mode를 제시했습니다.

구축 절차를 주말 작업에서 한 번의 설정으로 줄이기

저자는 두 번째 글의 샘플 저장소인 zenstack-code-mode-mcp가 작성 시점에 별 3개만 받은 이유를 사람들이 아이디어를 싫어해서라고 보지 않습니다. 저장소를 clone하고 의존성을 설치한 뒤 schema.zmodel을 자신의 데이터베이스에 맞게 다시 작성하고, 서버를 실행하고, MCP 클라이언트가 원격 서버에 직접 연결되는지 또는 mcp-remote 프록시가 필요한지 확인해야 합니다. 여기에 사용자 identity를 전달하는 방법도 결정해야 합니다. 첫 번째 방식에서는 OAuth client, authorization code, access token, refresh token 모델을 추가하고 세 개의 OAuth endpoint와 로그인 페이지를 구현하며 session transport를 관리해야 했습니다. 각각의 단계는 합리적이지만 모두 합치면 주말을 써야 하는 작업이 됩니다.

저자는 개발자가 여러 MCP 아키텍처를 비교하기보다 10분 안에 작동하는 첫 번째 방법을 선택하는 경향이 있다고 설명합니다. Claude에게 지난주 주문을 확인하게 하려는 개발자가 긴 설정을 요구받으면, 데이터베이스 connection string을 agent 설정에 직접 붙여 넣고 문제가 없기를 기대할 수 있다는 것입니다. 그래서 “설정할 수 없는 해결책은 블로그 글일 뿐입니다”라고 말하며, 이번 글의 목표를 접근성 개선으로 제시합니다.

ZenStack Studio는 ZenStack 프로젝트가 없어도 사용할 수 있습니다. 애플리케이션이 Prisma, Drizzle, Rails, Django 또는 일반 SQL로 만들어졌는지와 관계없이 Studio에는 데이터베이스만 필요합니다. 다음 명령을 실행하면 기존 데이터베이스를 introspect합니다.

npx @zenstackhq/cli studio --introspect

이 명령은 기존 PostgreSQL, MySQL 또는 SQLite 데이터베이스를 읽어 테이블과 관계를 설명하는 schema.zmodel을 생성하고, 로컬 머신에서 작은 proxy를 시작합니다. Studio는 이 proxy와 통신하고 proxy가 데이터베이스에 연결하므로 데이터베이스 credential은 로컬에 남으며 ZenStack으로 전송되지 않습니다. 애플리케이션 자체를 변경할 필요도 없습니다. 생성된 스키마에는 access policy가 포함되지 않은 단순한 테이블·관계 정보만 들어가며, 이는 Full access 모드로 agent를 연결하는 데 충분합니다.

프로젝트를 연 뒤 Table Editor와 Query Editor 옆의 MCP Server 항목을 켜면 MCP 클라이언트에 붙여 넣을 설정을 Studio가 제공합니다. 설정에는 Studio가 생성한 MCP server URL과 Bearer token이 들어갑니다.

``json { "mcpServers": { "zenstack": { "url": "<your MCP server URL, copied from Studio>", "headers": { "Authorization": "Bearer <token generated by Studio>" } } } } ``

proxy가 localhost에서만 실행되는 경우에는 @zenstackhq/studio-mcp-remote를 거치는 변형 설정을 제공합니다. 사용자는 설정을 복사해 붙여 넣은 뒤 Claude Code나 Cursor에 질문하면 됩니다. 저자는 앞선 두 글에서 직접 구현했던 기능이 이 한 번의 연결 뒤에 놓인다고 설명합니다.

`schema`, `check`, `execute`와 타입 검증

내부 구조는 이전 글의 Code Mode를 그대로 사용합니다. 데이터베이스에 모델이 5개이든 50개이든 agent가 받는 도구는 세 가지입니다. agent가 db.order.findMany({ ... }) 같은 호출을 만들면 check는 인자가 유효한지 추측하지 않습니다. 스키마의 타입으로 작은 TypeScript 프로그램을 메모리에 구성하고, agent가 만든 호출을 그 안에 넣은 뒤 compiler를 실행합니다. 따라서 필드 이름을 잘못 쓰거나 날짜가 들어가야 할 곳에 문자열을 전달하면 데이터베이스에 접근하기 전에 일반적인 editor와 같은 타입 오류를 돌려줍니다.

이전 글에서 Claude가 check를 호출하지 않았다는 지적에 “쿼리가 우연히 유효해서 운이 좋았지만, 올바른 접근은 아닙니다”라고 답한 사례도 이어집니다. prompt에 MUST를 추가하면 check 사용을 유도할 수 있지만 prompt는 요청일 뿐 보장이 아닙니다. 그래서 Studio의 execute는 자체적으로 permission check를 다시 수행합니다. agent가 check 단계를 건너뛰면 유용한 타입 오류를 먼저 받지 못할 뿐이고, 단계를 생략해 권한 규칙을 우회할 수는 없습니다. 저자는 이를 “high cohesion, low coupling” 원칙을 한 단계 더 적용한 것으로 설명합니다.

모델별 노출 범위와 관계 탐색 차단

Exposed Models 페이지에서는 각 모델에 대해 read, insert, update, delete 스위치를 개별적으로 설정할 수 있습니다. 네 스위치를 모두 끈 모델은 존재하지 않는 것처럼 schema 도구의 반환 결과에서 사라집니다. 그러나 관계를 따라가는 query API의 강력함이 MCP에서는 우회 경로가 될 수 있으므로, 최상위 모델만 확인해서는 안 됩니다.

예를 들어 Order는 노출했지만 Customer는 노출하지 않은 상태에서 다음과 같은 요청을 보낼 수 있습니다.

db.order.findMany({ include: { customer: true } })

이 경우 check와 execute는 include와 select를 따라가며 접근 대상 전체를 확인하고, “Model 'Customer' is not exposed by this MCP server.”라는 오류를 반환합니다. 즉 모델 스위치는 agent가 직접 호출할 수 있는 모델뿐 아니라 관계를 통해 읽거나 수정하려는 모델에도 적용됩니다.

Full access와 특정 사용자 권한

Studio에서 MCP 연결 방식은 Full access와 Specific user 두 가지를 제공합니다. Full access는 어떤 규칙도 적용하지 않으며, 자신의 데이터베이스를 coding agent에 연결하려는 경우에 적합합니다. Specific user를 선택하면 모든 query가 해당 사용자가 직접 실행한 것처럼 처리되어 access policy가 적용됩니다.

introspection으로 생성된 스키마에는 policy가 없기 때문에, 사용자가 직접 ZModel 문법을 배워 정책을 작성할 수도 있습니다. 또는 다음 명령으로 ZenStack skills를 설치한 뒤 agent에게 작성을 요청할 수 있습니다.

npx skills add zenstackhq/skills

예를 들어 “사용자는 자신의 주문만 읽을 수 있게 하고 support staff는 모든 주문을 읽을 수 있도록 schema.zmodel에 access policy를 추가해 달라”고 요청할 수 있습니다. 예시 정책에서는 User 모델에 @@allow('read', auth() == this)를 넣어 자기 프로필을 읽게 하고, Order 모델에는 @@allow('read', auth() == owner)를 넣어 자신의 주문만 읽게 합니다. 동시에 @@allow('read', auth().role == 'support')를 추가해 support 역할에는 전체 주문을 허용합니다.

proxy가 수정된 스키마로 실행되면 MCP Server 페이지에서 스키마를 새로 고칩니다. Studio는 agent가 보는 스키마를 교체하기 전에 변경 diff를 보여줍니다. 이후 { "id": "user_42" }로 연결한 상태에서 agent가 “지난달 총 지출”을 물으면 user_42의 주문만 보게 됩니다. prompt가 잘못된 where 조건으로 모든 주문을 요청하더라도 정책이 이를 제한하며, prompt의 지시에 의존하지 않고 query 실행 자체에 규칙이 적용됩니다.

요청별 identity와 stateless MCP 서버

사용자 identity는 Authorization header의 token으로 MCP 서버에 전달됩니다. 이 token은 프로젝트 secret key로 서명되며 Studio가 생성할 수 있고, 애플리케이션 코드도 필요할 때 사용자별로 생성할 수 있습니다. 글의 예시는 HS256 JWT 형식의 header와 payload를 Base64URL로 인코딩하고, crypto.createHmac('sha256', ...)로 서명합니다. payload의 data는 access policy에서 auth()가 해석할 사용자 객체이며, 예시 함수는 사용자의 id를 넣어 type: 'user'와 함께 token을 반환합니다.

이 방식에서는 사용자마다 MCP 서버를 만들거나, 사용자를 미리 등록하거나, 별도의 로그인 흐름을 거칠 필요가 없습니다. 서버는 하나이고 호출자가 요청마다 어떤 사용자로 agent를 실행할지 결정합니다. access policy가 그 identity를 기준으로 나머지 접근을 제한합니다.

글은 2026-07-28 specification release가 initialize handshake와 session ID를 제거해 각 요청이 독립적으로 처리되도록 했다고 설명합니다. 해당 release 글은 “Stateless core makes MCP a first-class HTTP workload with no session management to work around.”라고 표현합니다. 이전 구현에서 필요했던 session transport map이 사라지는 이유도 여기에 있습니다. Studio의 MCP 서버 역시 stateless이며, identity가 매 요청에 포함되므로 어떤 서버 인스턴스도 별도의 조회 없이 어떤 사용자든 처리할 수 있습니다.

저자는 이 구조의 적용 예로 세 가지를 듭니다. Support engineer가 고객의 ticket을 조사할 때 해당 고객의 token으로 Claude Code를 실행하면 staging copy가 아니라 그 고객이 볼 수 있는 데이터만 확인할 수 있습니다. Background agent는 조직별 token을 순서대로 서명해 각 tenant의 주간 요약을 만들 수 있으며, 한 tenant의 prompt가 잘못되어도 다른 tenant의 행을 읽을 수 없습니다. 제품 내부 assistant는 현재 사용자용 token으로 연결되어 사용자의 Trello board만 바탕으로 “지난주에 완료한 카드가 몇 개인가” 같은 질문에 답할 수 있습니다.

결국 시작점은 npx @zenstackhq/cli studio 명령이며, Studio를 연 뒤 필요에 따라 npx skills add zenstackhq/skills로 access policy 작성을 agent에 맡길 수 있습니다. 저자는 첫 번째 글에서 여러 파일과 구현 단계로 보여준 Trello 예제가 이번에는 함수 하나와 몇 가지 policy 규칙으로 축소됐다고 설명하며, 시리즈의 흐름을 “Make it work, make it right, make it easy”로 정리합니다.

원문: dev.to / 번역·요약: Trawling

참고 자료

오류 제보