Laravel 앱 내부에 MCP 서버 구축하기: AI 도구를 위한 이중 인증 및 RBAC
요약
Laravel 애플리케이션 내에 Model Context Protocol(MCP) 서버를 구축하여 AI 에이전트가 안전하게 앱의 도구를 호출할 수 있도록 하는 방법을 다룹니다. 단일 엔드포인트 전략과 Sanctum 및 OAuth 2.1을 활용한 이중 인증 체계를 통해 보안과 확장성을 동시에 확보하는 설계 패턴을 제시합니다.
핵심 포인트
- MCP를 통해 AI 에이전트 전용 '안내 데스크' 역할을 하는 타입화된 도구 노출
- 단일 /mcp 엔드포인트 통합을 통한 보안 및 관리 효율성 증대
- Sanctum과 OAuth 2.1을 결합한 이중 인증 체계 구축 권장
- 직접 구현 대신 패키지를 활용하여 인증 및 도구 구현에 집중
모든 앱의 생애 주기에는 누군가가 다음과 같이 질문하는 순간이 있습니다: "AI 어시스턴트가 이 서비스와 그냥 _대화_할 수 있을까요?" UI를 스크래핑하거나 REST API를 역공학(Reverse-engineer)하는 것이 아니라, 인간 사용자가 거치는 것과 동일한 권한 규칙을 따르면서 앱이 의도적으로 노출하는 타입화된 도구(Typed tools)를 실제로 호출하는 것을 말합니다. 이것이 바로 Model Context Protocol (MCP)가 해결하는 문제이며, 오늘 작업한 내용은 적절한 MCP 서버를 Laravel 애플리케이션에 직접 연결하는 것이었습니다.
저는 이 내용을 교육적이고 범용적으로 유지할 것입니다. 여기서 다루는 패턴은 제가 작업하던 특정 제품이 아니라 모든 Laravel 앱에 적용됩니다. 제가 내린 결정들을 살펴보겠습니다. 흥미로운 부분은 "도구를 어떻게 등록하는가"(그건 쉬운 부분입니다)가 아니라, 인증(Auth), 인가(Authorization), 그리고 그 경계(Seams)를 어디에 두느냐 하는 문제였기 때문입니다.
비유: MCP 서버는 뒷문이 아니라 안내 데스크입니다
여러분의 앱을 사무용 빌딩이라고 생각해 보세요. 웹 UI는 인간을 위한 정문 로비입니다. REST API는 다른 시스템을 위한 직원용 입구입니다. MCP 서버는 _AI 에이전트를 위해 특별히 구축된 안내 데스크_입니다. 에이전트가 다가와 신분증을 보여주고 안내 데스크 직원에게 "이 이벤트의 참가자 목록을 보여줘", "이 사용자의 상세 정보를 가져와줘"와 같이 이름이 지정된 작업을 요청합니다. 안내 데스크 직원은 건물 열쇠를 통째로 넘겨주지 않습니다. 당신이 누구인지 확인하고, 무엇을 할 수 있는지 확인한 뒤, 정확히 그 한 가지 작업만 수행하고 구조화된 답변을 돌려줍니다.
MCP를 이해하게 만드는 사고의 전환은 이것입니다: 여러분은 데이터베이스나 내부 구조를 노출하는 것이 아닙니다. 여러분은 의도의 메뉴(Menu of intentions) — 즉, 개별적이고 이름이 지정된 타입화된 도구들 — 를 노출하는 것이며, 그 외의 모든 것은 데스크 뒤에 머물러 있습니다.
경로의 숲이 아닌, 단 하나의 엔드포인트
첫 번째 실제 결정은 모든 것을 단일 /mcp 엔드포인트로 통합하는 것이었습니다. 초기에는 여기저기 경로를 늘려놓고 싶은 유혹이 생깁니다. 여기엔 라우트 하나, 저기엔 설정 페이지 하나, 다른 곳엔 토큰 관련 기능 하나 식이죠. 저는 프로토콜이 통신하는 하나의 MCP 엔드포인트로 모든 것을 모았고, 인간이 사용하는 설정은 시스템의 나머지 구성이 위치한 관리자(Admin) 영역 아래에 유지했습니다.
그 이유는 15개의 서로 다른 웹훅(Webhook) URL을 노출하지 않는 것과 같은 이치입니다. 잘 정의된 하나의 접점(Surface)은 흩어져 있는 여러 개의 접점보다 보안을 유지하고, 문서화하며, 구조를 파악하기가 훨씬 쉽습니다. 프로토콜은 이미 도구 발견(Tool discovery) 기능을 제공합니다. 즉, 클라이언트가 서버에 "무엇을 할 수 있나요?"라고 물으면 서버는 목록을 반환합니다. 따라서 기능별로 별도의 라우트(Route)를 만들 필요가 없습니다. 도구들이 스스로를 등록하며, 엔드포인트(Endpoint)는 단일하게 유지됩니다.
프로토콜의 배관 작업(Plumbing)을 직접 구현하기보다는 패키지를 활용하세요. 저는 Laravel의 MCP 지원 기능과 제가 만든 cleaniquecoders/laravel-mcp-kit를 함께 사용하여 서버 등록, 도구 베이스 클래스, 토큰 테이블과 같은 빠른 시작용 스캐폴딩(Scaffolding)을 구축했습니다. 덕분에 프로토콜의 메커니즘 대신 인증(Auth)과 실제 도구 구현에 집중할 수 있었습니다.
이중 인증: Sanctum 및 OAuth 2.1
이 부분은 제가 특히 강조하고 싶은 부분입니다. 왜냐하면 대부분의 "그냥 AI 엔드포인트를 덧붙이는" 시도들이 이 지점에서 허술해지기 때문입니다.
MCP 서버에는 매우 다른 두 가지 종류의 호출자(Caller)가 있습니다:
- 퍼스트 파티 (First-party) 클라이언트 — 여러분의 백그라운드 작업(Background job), CLI, 또는 내부 통합 시스템입니다. 이 경우에는 개인 액세스 토큰(Personal access token, Sanctum)이 적합한 도구입니다. 단순하고, 취소 가능하며, 범위(Scoped)를 지정할 수 있습니다.
- 서드 파티 (Third-party) 에이전트 — 사용자를 대신하여 동작하는 에이전트, 즉 사용자가 외부에서 연결하는 AI 클라이언트입니다. 이 경우에는 실제 위임된 권한 부여(Delegated-authorization) 흐름이 필요하며, 이것이 바로 OAuth 2.1의 용도입니다. 사용자가 동의하면 에이전트는 해당 사용자에게 귀속된 토큰을 받게 되며, 여러분은 다른 어떤 것도 건드리지 않고 해당 토큰을 취소할 수 있습니다.
하나만 선택하여 호출자의 절반을 불편하게 만드는 대신, 서버는 두 가지 모두를 수용합니다. 핵심은 해결(Resolution) 과정을 통일하는 것입니다. 요청을 인증한 경로가 무엇이든, 도구 내부로 들어왔을 때는 인증된 사용자라는 단 하나의 객체만을 가지게 되며, 도구 코드는 사용자가 어떻게 인증되었는지 신경 쓸 필요가 없습니다.
드라이버 스타일의 가드 리졸버(Guard resolver)를 사용하면 이를 깔끔하게 처리할 수 있습니다. 개념적으로는 다음과 같습니다:
interface ResolvesMcpIdentity
{
public function resolve(Request $request): ?Authenticatable;
...
각 스킴(scheme)당 하나의 리졸버(resolver)를 구현하고(Sanctum용 하나, OAuth용 하나), 순서대로 시도하여 처음으로 성공하는 것을 선택합니다. 나중에 새로운 스킴이 추가되나요? 드라이버(driver)를 추가하고 등록하기만 하면 끝납니다. 도구들은 전혀 변하지 않습니다. 이것은 제가 어디에서나 의존하는 '교체 가능한 백엔드(swappable-backend)' 패턴과 동일합니다. 즉, 계약(contract)을 기준으로 프로그래밍하고, 구현(implementation)을 다양화하는 것입니다.
권한 부여(Authorization)는 인증(authentication)이 아닙니다 — 권한(abilities)을 도구에 매핑하세요
호출자를 인증하는 것은 _누구인지_를 알려줄 뿐입니다. 그들이 _무엇을 할 수 있는지_에 대해서는 아무것도 말해주지 않습니다. 제가 끊임없이 목격하는 실수는 이렇습니다. 앱이 MCP 연결을 신중하게 인증한 다음, 인증된 호출자라면 누구나 어떤 도구든 호출할 수 있게 허용하는 것입니다. 이는 안내 데스크 복장을 하고 있는 뒷문(back door)과 같습니다.
따라서 모든 도구는 권한(ability)에 매핑되며, 웹 UI에서 적용되는 것과 동일한 RBAC(역할 기반 액세스 제어) 규칙이 여기에서도 적용됩니다. 이를 표현하는 가장 깔끔한 방법은 각 도구가 자신에게 무엇이 필요한지 선언하도록 하는 것입니다:
final class ListEventParticipantsTool extends Tool
{
public function ability(): string
...
도구가 전체 Eloquent 모델이 아니라, 의도적으로 좁혀진 투영(deliberately narrowed projection) — UUID 공개 ID, 이름, 상태 — 를 반환한다는 점에 주목하세요. 도구는 외부 접점(external surface)입니다. 도구의 출력을 내부 데이터 덤프(internal dump)가 아닌 API 리소스처럼 취급하십시오. 이는 컨트롤러에서 $model->toArray()를 직접 반환하지 않는 것과 동일한 규율입니다.
권한 체크는 ability()를 읽고 기존 게이트(gate)를 통해 실행하는 미들웨어(middleware)나 베이스 도구 훅(base-tool hook)과 같은 한 곳에서 이루어집니다. 단일 병목 지점(choke point)을 통해 모든 도구를 커버하며, 도구마다 if 문이 난무하는 상황을 방지합니다.
개념적인 디렉토리 도구들
제가 실제로 배포한 도구들은 작은 "디렉토리(directory)" 세트였습니다. 이벤트 참가자 목록 조회, 조직의 사용자 목록 조회, 단일 사용자 조회, 모든 사용자 목록 조회 등이 그것입니다. 의도적으로 지루하게 만들었습니다. 그것이 핵심입니다. MCP 도구는 이미 다른 곳에서 노출하고 있는 데이터에 대해 _얇고(thin), 이름이 명확하며, 권한이 잘 부여된 동사(verb)_여야지, 자체적인 규칙을 가진 영리하고 새로운 경로가 되어서는 안 됩니다. 각 도구는 Form Request를 통해 입력을 검증하고, 권한(ability)을 확인하며, 정제된 투영(projection)을 반환합니다. 만약 어떤 도구가 앱의 나머지 부분에서는 허용하지 않는 특수한 접근 권한을 요구하기 시작한다면, 그것은 설계상의 결함(smell)입니다. 해결책은 도구에 비밀 통로를 만들어 주는 것이 아니라 권한 모델(permission model)을 수정하는 것입니다.
해피 패스(happy path)가 아닌 경계(boundary)를 테스트하라
여기서 가치 있는 테스트는 "도구가 데이터를 반환하는가"가 아니라, "도구가 거부해야 할 때 _거부하는가"입니다. Pest를 사용하면 부정적인 케이스(negative cases)를 읽기 쉽게 작성할 수 있습니다:
it('rejects an authenticated user who lacks the ability', function () {
$user = User::factory()->create(); // no participants.view
...
제가 가장 신경 쓰는 것은 not->toHaveKey('email') 단언(assertion)입니다. 이것은 회귀 테스트 방어벽(regression fence) 역할을 합니다. 누군가가 "친절하게도" 도구에서 모델 전체를 반환하는 날, 이 테스트는 배포되기 전에 빨간색(실패)으로 변할 것입니다. 자율 에이전트(autonomous agent)가 당신의 인터페이스를 소비할 때, 테스트 스위트는 부주의한 데이터 투영과 데이터 유출 사이를 막아주는 유일한 수단입니다.
토큰 + 설정 UI: 권한 취소는 인간의 속도로 이루어져야 하므로
마지막 요소는 MCP 서버를 켜거나 끌 수 있고 토큰을 관리할 수 있는 작은 관리자 UI입니다. 이것은 단순히 편의 기능처럼 들릴 수 있지만, 실제로는 보안 모델의 일부입니다. 오작동하는 에이전트를 차단하는 유일한 방법이 SSH로 접속하여 Tinker 명령어를 실행하는 것이라면, 당신은 버튼이 없는 화재 경보기를 만든 것과 같습니다. 사람은 몇 번의 클릭만으로 "이 토큰들이 존재하고, 이 토큰은 문제가 있어 보이니, 폐기하겠다"라고 판단할 수 있어야 합니다. 킬 스위치(kill switch)는 압박을 받는 상황에서도 사람이 쉽게 찾을 수 있는 곳에 두어야 합니다.
요약(Takeaway)
Laravel 앱에 MCP 서버를 결합하는 것은 쉽습니다. 하지만 이를 책임감 있게 수행하는 것은 세 가지 경계에 달려 있습니다: 모든 호출자를 인증하고(이때 퍼스트 파티(first-party)와 서드 파티(third-party) 호출자가 서로 다른 스킴(scheme)을 원한다는 점을 인정해야 합니다), 이미 보유한 권한 모델에 따라 모든 도구(tool)를 인가(authorize)하며, 모든 도구의 출력을 공개 API 리소스처럼 취급하는 것입니다. 이 세 가지를 올바르게 설정하면 AI 어시스턴트는 창문을 통해 몰래 들어온 침입자가 아니라, 안내 데스크에서 예의 바르게 행동하는 손님이 됩니다.
다음 단계: OAuth 경로의 동의 화면(consent screen)을 강화하고, 잘 인가된 동사(verb)를 하나씩 신중하게 추가하며 도구 메뉴를 확장해 나가는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기