Laravel AI Agent를 모든 MCP 서버에 연결하기: 실습 가이드
요약
Laravel AI 에이전트가 MCP(Model Context Protocol) 서버에 클라이언트로 연결되어 외부 도구를 사용할 수 있도록 하는 실습 가이드입니다. GitHub, Notion 등 다양한 MCP 서버의 도구를 에이전트가 직접 호출하여 활용하는 방법을 다룹니다.
핵심 포인트
- Laravel AI 에이전트를 MCP 클라이언트로 전환하여 외부 도구 통합 가능
- laravel/mcp 패키지를 통해 서버 연결, 인증 및 도구 호출 수행
- laravel/ai 패키지로 기존 에이전트의 도구 계약에 MCP 도구 래핑
- PHP 8.3 이상 및 Laravel 12/13 환경 권장
원문은 hafiz.dev에 게시되었습니다.
대부분의 Laravel MCP 튜토리얼은 서버를 구축하는 방법을 가르칩니다. 앱의 도구(tools)를 노출하면 AI 클라이언트가 연결되어 Claude나 Cursor가 귀하의 주문 테이블(orders table)을 조회할 수 있게 됩니다. 이는 유용하며, 저는 이전에 Filament를 사용하여 MCP 서버 구축하기를 다룬 적이 있습니다.
이 포스트는 반대 방향으로 진행합니다. 귀하의 Laravel AI 에이전트(agent)가 클라이언트(client)가 되는 것입니다. 에이전트는 GitHub, Notion, Laravel Nightwatch 또는 로컬에서 실행 중인 서버 등 어떤 MCP 서버로든 연결할 수 있으며, 마치 직접 작성한 것처럼 해당 서버의 도구들을 사용합니다. 이미 Laravel AI SDK로 에이전트 구축하기를 완료했다면, 이 기능은 기존 에이전트에 바로 끼워 넣을 수 있습니다. 에이전트에게 "내 앱의 최신 예외(exception)를 확인해줘"라고 요청하면, 에이전트는 Nightwatch 이슈를 탐색하고, 스택 트레이스(stack trace)를 가져와서 보고합니다. 귀하는 그 통합(integration) 코드를 전혀 작성하지 않았습니다.
Laravel은 이를 2026년 6월 9일에 출시했습니다. 이는 에이전트를 단순히 귀하의 코드만 아는 존재에서, 팀이 이미 사용 중인 모든 도구에 접근할 수 있는 존재로 탈바꿈시키는 핵심 요소입니다. 만약 Laravel Boost와 MCP를 통해 에이전트에 컨텍스트(context)를 제공해 왔다면, 이것은 그 반대 과정입니다. 즉, 도구가 안으로 들어오는 것이 아니라 에이전트가 밖으로 뻗어 나가는 것입니다. 인증이 필요 없는 로컬 서버부터 전체 OAuth 흐름에 이르기까지, 이를 연결하는 방법과 운영 환경(production)에서 주의해야 할 사항들을 소개합니다.
패키지에 실제로 포함된 내용
이 기능은 두 개의 패키지가 함께 작동하며, 그 분리가 중요합니다.
MCP 클라이언트(client)는 laravel/mcp에 들어 있습니다. 이 패키지는 서버에 연결하고, 핸드셰이크(handshake)를 협상하며, 인증하고, 도구(tools)를 호출하는 방법을 알고 있습니다. 에이전트가 전혀 없는 상태에서도 큐 작업(queued job)이나 콘솔 명령(console command)을 통해 단독으로 작동합니다.
이 얇은 통합 계층(thin integration)은 laravel/ai에 구현되어 있습니다. 이를 통해 에이전트가 기존 작동 방식을 변경하지 않고도 해당 클라이언트를 사용할 수 있습니다. 이미 사용 중인 tools() 배열에 MCP 도구들을 넣기만 하면, SDK가 에이전트의 도구 계약(tool contract)에 맞게 각 도구를 래핑(wrap)합니다.
코드를 작성하기 전에 버전을 고정하세요. 2026년 6월 말 기준:
laravel/mcpv0.8.1 (0.8 라인에서 클라이언트가 도입되었습니다. 이전 버전은 서버 전용이었습니다)laravel/aiv0.8.1- Laravel 12 또는 13
- PHP 8.3 이상
이 PHP 최소 요구 사항은 중요합니다. laravel/mcp 자체는 PHP 8.2를 허용하지만, laravel/ai는 8.3을 요구하며, 이 작업을 위해서는 두 가지 모두가 필요합니다. 만약 8.2 버전을 사용 중이라면 Composer가 종속성 해결(resolve)을 거부할 것입니다. 현재 설치된 버전을 확인하세요:
composer show laravel/mcp laravel/ai
클라이언트 인터페이스 (The Client Surface)
클라이언트의 전체 기능은 세 가지 동사로 요약됩니다: 연결(connect), 도구 목록 조회(list tools), 도구 호출(call a tool). 그 외의 모든 것은 사용자의 작업에 간섭하지 않습니다.
use Laravel\/Mcp\/Client;
// HTTP를 통한 원격 서버
...
두 가지 생성자는 두 가지 전송 방식(transports)을 의미합니다. web()은 네트워크를 통해 접근하는 원격 서버를 위해 스트리밍 가능한 HTTP를 사용합니다. local()은 동일한 머신에서 자식 프로세스로 실행되는 서버를 위해 STDIO (표준 입출력)를 사용합니다. 선호도가 아닌 서버가 어디에 위치하느냐에 따라 선택하면 됩니다.
도구를 목록화하고 호출하는 방식은 다음과 같습니다:
$tools = $client->tools();
$result = $client->callTool('search-issues', [
...
연결은 첫 번째 실제 호출 시 지연(lazily)되어 발생하므로, 수동으로 관리할 필요가 없습니다. 하지만 원한다면 직접 관리할 수도 있습니다:
$client->connect();
$client->ping();
$client->disconnect();
연결 과정에서 클라이언트와 서버는 프로토콜 버전(protocol version)을 협상하고 기능(capabilities)을 교환하여, 작업이 시작되기 전에 양측이 규칙에 합의합니다. 이 과정은 직접 작성할 필요가 없습니다. 이는 핸드셰이크(handshake) 과정이며, 만약 실패한다면(이에 대해서는 나중에 자세히 다룹니다) 대개 내부적인 인증(auth) 문제나 버전 불일치 때문입니다.
1단계: 인증이 없는 로컬 서버
복잡함에 직면하기 전에 연결 구조를 파악할 수 있도록 가장 단순한 사례부터 시작합니다. 인증이 전혀 필요 없는 로컬 MCP 서버(본인의 서버 또는 모든 STDIO 서버)를 에이전트에 연결해 보세요.
에이전트 내부에서 MCP 도구(tools)는 직접 작성한 도구 바로 옆의 tools() 메서드에 그대로 들어갑니다:
use App\Ai\Tools\SendSlackMessage;
use Laravel\Mcp\Client;
...
이것이 통합의 전부입니다. 스프레드 연산자(spread operator)가 MCP 서버의 모든 도구를 배열에 집어넣습니다. SDK는 이것들이 MCP 도구임을 감지하고 각각을 래핑(wrap)합니다. 즉, MCP 입력 스키마(input schema)를 Laravel의 JSON 스키마로 변환하고, 모델이 요청할 때 원격 도구를 호출하며, 반환되는 모든 내용을 모델이 읽을 수 있는 결과로 정규화(normalize)합니다. 모델은 어떤 도구가 MCP에서 왔고 어떤 도구를 직접 작성했는지 알 수 없습니다. 모델에게는 모두 동일하게 보입니다.
이것이 전체 기능의 멘탈 모델(mental model)입니다. 로컬 서버 하나를 연결할 수 있다면, 다른 모든 서버는 단지 다른 전송 방식(transport)과 그 위에 추가된 인증 계층(auth layer)일 뿐입니다.
2단계: Bearer Token 인증
대부분의 유용한 서버는 사용자가 누구인지 알고 싶어 합니다. 더 간단한 인증 경로는 이미 보유하고 있는 문자열인 Bearer 토큰(bearer token)을 사용하는 것입니다. GitHub의 개인 액세스 토큰(personal access token)이 전형적인 예입니다.
use Laravel\Mcp\Client;
$client = Client::web('https://api.githubcopilot.com/mcp/')
...
토큰이 현재 인증된 사용자에게 속해 있는 경우, 생 문자열(raw string) 대신 클로저(closure)를 전달할 수 있습니다:
$client = Client::web('https://api.githubcopilot.com/mcp/')
->withToken(fn () => auth()->user()->github_token);
에이전트에 적용하는 방법은 동일합니다:
public function tools(): iterable
{
return [
...
이제 에이전트는 저장소를 검색하고, CI 로그를 읽고, 이슈를 분류(triage)하며, Dependabot 알림을 확인할 수 있습니다. 에이전트가 아무것도 변경하지 않기를 원한다면, GitHub 서버는 요청 헤더(request header)를 통해 읽기 전용(read-only) 모드도 지원합니다.
3단계: 전체 OAuth (실제 사례)
이미 토큰을 보유하고 있는 경우에는 Bearer 토큰을 사용해도 괜찮습니다. 하지만 Laravel Nightwatch와 같이 호스팅된 서버는 OAuth를 사용합니다. 이 방식에서는 사용자가 버튼을 클릭하고, 제공자(provider)의 사이트에서 액세스 권한을 승인한 뒤, 앱이 저장할 수 있는 토큰을 가지고 다시 돌아옵니다. (만약 Nightwatch를 다른 대안들과 비교하고 있다면, 제가 별도로 Telescope vs Pulse vs Nightwatch를 비교해 두었습니다.) 이 부분은 공식 발표에서 대략적으로 넘어간 내용이므로, 전체 흐름을 설명하겠습니다.
보통 서비스 프로바이더(service provider)의 boot() 메서드에서 이름이 지정된 클라이언트(named client)를 등록합니다:
use Laravel\/Mcp\/Client;
use Laravel\/Mcp\/Facades\/Mcp;
...
만약 서버가 동적 클라이언트 등록(dynamic client registration)을 지원한다면, ID와 Secret을 완전히 생략할 수 있으며 클라이언트가 스스로를 등록합니다. 이 기능의 가능 여부는 서버에 따라 다르며, 이는 문제가 발생했을 때 중요한 요소가 됩니다.
다음으로, routes/ai.php에서 연결(connect) 및 콜백(callback) 라우트를 연결합니다. 패키지에서 두 가지를 모두 등록해 주는 헬퍼(helper)를 제공합니다:
use Laravel\/Mcp\/Client\/OAuth\/TokenSet;
use Laravel\/Mcp\/Facades\/Mcp;
...
이렇게 하면 mcp.oauth.nightwatch.connect와 mcp.oauth.nightwatch.callback이라는 두 개의 이름이 지정된 라우트(named routes)가 등록됩니다. connect 라우트는 사용자를 인증 서버(authorization server)로 리다이렉트합니다. callback 라우트는 인증 코드(authorization code)를 교환하고 새로운 토큰과 함께 클로저(closure)를 실행합니다. 개발자는 리다이렉트 URL이나 PKCE 세부 사항을 직접 다룰 필요가 없습니다.
그 후, Blade 뷰의 버튼이 이 과정을 시작합니다:
<a href="{{ route('mcp.oauth.nightwatch.connect') }}">
Connect Nightwatch
</a>
흐름은 순서대로 진행됩니다: 사용자가 버튼을 클릭하면, 패키지가 사용자를 Nightwatch로 리다이렉트하고, 사용자가 로그인 및 승인을 하면, Nightwatch가 사용자를 다시 콜백 라우트로 보내며, 토큰과 함께 클로저가 실행되고, 여러분은 원하는 방식으로 토큰을 저장합니다. 패키지가 OAuth 안무(choreography)를 담당하고, 여러분의 앱은 토큰이 저장될 위치를 담당합니다.
한 가지 명확히 해둘 점은, 내장된 토큰 저장소(token store)가 없다는 것입니다. 여러분이 직접 $token->accessToken을 영구 저장(persist)해야 하며, 클라이언트를 구축할 때 이를 다시 읽어와야 합니다. 패키지는 만료된 토큰을 갱신(refreshing)하는 작업을 처리하지만, 저장은 여러분의 책임입니다.
토큰이 저장되면, 에이전트는 명명된 클라이언트를 사용합니다:
public function tools(): iterable
{
return [
...
이제 에이전트에게 다음과 같이 물어보세요: "운영 환경(production)에서 가장 최근에 발생한 예외(exceptions)는 무엇인가요? 어떤 것을 먼저 수정해야 할지 우선순위를 정하는 것을 도와주세요." 그러면 에이전트는 이슈를 탐색하고, 스택 트레이스(stack traces)를 읽고, 답변을 제공합니다. 또한 "이슈 123을 수정 내용을 요약한 코멘트와 함께 해결됨(resolved)으로 표시해줘"라고 말하면 그 작업도 수행합니다.
에이전트가 MCP 도구를 바라보는 방식
모델이 Nightwatch 도구를 사용하기로 결정했을 때 어떤 일이 일어나는지 알 수 있도록, 프롬프트부터 원격 도구 호출(remote tool call) 및 그 반대 과정까지의 흐름을 설명하겠습니다.
핵심 통찰은 중간 단계에 있습니다. 모델이 선택을 내리는 시점에는 네이티브 도구와 MCP 도구가 동일한 리스트에 놓여 있으며 동일하게 보입니다. 래핑(wrapping), 스키마 변환(schema translation), 네트워크 호출(network call), 결과 정규화(result normalization) 등 이 모든 과정은 모델이 인지하지 못하는 하위 계층에서 발생합니다.
도구 목록 캐싱하기
도구 목록을 나열하는 것은 원격 서버에 대한 네트워크 왕복(network round trip)을 수반하며, 도구 목록은 거의 변경되지 않습니다. 따라서 이를 캐싱(cache)하십시오. 도구는 일반 데이터(plain data) 형태로 반환되므로, Laravel의 캐시로 호출을 감싸면 깔끔하게 재수화(rehydrate)됩니다:
use Illuminate\Support\Facades\Cache;
$tools = Cache::remember('mcp.nightwatch.tools', now()->addHour(), function () {
...
알아둘 점: 일부 설명이 암시하는 바와 달리, 이것은 전용 클라이언트 기능이 아닙니다. tools() 호출을 감싸는 표준 Laravel 캐싱입니다. 하지만 이점은 확실합니다. 매 프롬프트마다 발생하는 네트워크 왕복을 건너뛸 수 있으며, 이는 각 호출마다 인증 오버헤드(auth overhead)가 발생하는 OAuth 보안 원격 서버에서 매우 중요합니다.
라이브 서버 없이 테스트하기
테스트 스위트(test suite)가 네트워크를 통해 실제 MCP 서버에 접속하게 하고 싶지는 않을 것입니다. Laravel AI의 페이킹 레이어(faking layer)를 사용하면, 도구 호출(tool call)은 여전히 MCP 레이어를 통해 흐르도록 유지하면서 모델의 응답을 스크립트로 작성할 수 있습니다.
MCP 도구 이름은 mcp_tools_<name> 패턴을 따릅니다. search라는 이름의 서버 도구는 에이전트에게 mcp_tools_search로 나타납니다. 따라서 도구를 호출하는 모델 응답과 최종 답변을 페이크(fake)로 만든 뒤, 그 결과에 대해 단언(assert)할 수 있습니다:
use Laravel\\\Ai\\\
Facades\\\
Ai;
it('investigates exceptions through the nightwatch agent', function () {
...
단 한 번의 네트워크 호출 없이도 프로덕션 경로(에이전트가 도구를 호출하고, 결과를 읽고, 응답하는 과정)를 테스트할 수 있습니다.
실제로 당신을 괴롭힐 수 있는 주의사항들
해피 패스(happy path)는 깔끔합니다. 하지만 개발자들이 실제로 겪었던 문제들을 바탕으로, 그렇지 않은 부분들을 정리했습니다.
OAuth 핸드셰이크(handshake) 실패가 가장 큰 문제입니다. 특정 에이전트를 통해 Nightwatch를 연결할 때, 인증 전에는 403 Forbidden ... when send initialize request가 발생하고, 인증 후에는 핸드셰이크 디코딩 오류가 발생하는 문서화된 사례(laravel/boost issue #584)가 있습니다. 보고된 원인은 클라이언트가 인증이 필요함을 감지하지 못해 로그인 흐름을 트리거하지 않았기 때문입니다. 해당 스레드에서는 두 가지 해결책이 제시되었습니다. 에이전트가 지원한다면 명시적인 로그인을 실행하십시오. 또는 URL을 직접 가리키는 대신 mcp-remote 브릿지(bridge)를 통해 라우팅하십시오. 이는 여러 클라이언트에 대해 Nightwatch 자체 문서에서도 권장하는 방식입니다. 직접 연결 시 핸드셰이크에 실패한다면, 코드가 잘못되었다고 가정하기 전에 브릿지를 먼저 시도해 보십시오.
동적 클라이언트 등록(Dynamic client registration)은 보편적이지 않습니다. 클라이언트 ID와 비밀번호(secret)를 생략할 수 있었던 OAuth 단계를 기억하시나요? 이는 서버가 동적 클라이언트 등록을 지원할 때만 작동합니다. 여러 서버는 사전 등록된 OAuth 클라이언트를 요구하며, 이들에 대해 자동 등록을 시도하면 종종 인증 버그처럼 보이는 403 오류와 함께 실패합니다. 대상 서버가 어떤 모드를 기대하는지 파악하십시오. 동적 등록을 지원하지 않는다면, clientId와 clientSecret을 명시적으로 제공해야 합니다.
토큰 저장(Token storage)은 여러분의 역할이며, 실수하기 쉽습니다. 이 패키지는 토큰을 갱신(refresh)하지만 저장하지는 않습니다. 흔히 하는 실수는 withToken() 클로저(closure)가 다시 읽어올 수 없는 곳에 토큰을 영구 저장하거나, 아예 저장하지 않아서 매 요청마다 왜 인증 재요청(re-prompt)이 발생하는지 의아해하는 것입니다.
프로토콜 버전 불일치(Protocol version mismatches)는 핸드셰이크(handshake) 실패로 나타납니다. 서버들은 서로 다른 MCP 스펙 버전을 광고합니다. 만약 서버가 클라이언트와 협상(negotiate)되지 않는 버전만 지원한다면, 연결은 핸드셰이크 단계에서 실패합니다. 오류 메시지에 항상 "버전"이라는 단어가 포함되지는 않으므로, 연결이 수립되지 않을 때는 이를 의심 목록에 포함해 두십시오.
원격 도구(Remote tool) 오류는 여러분의 코드 내로 예외를 던지지 않습니다. 원격 도구가 실패할 때, 결과값은 에이전트 루프(agent loop)에서 예외(exception)를 발생시키는 대신 isError를 포함합니다. 래퍼(wrapper)는 이 실패를 모델이 읽고 반응할 수 있는 형태로 정규화(normalize)합니다. 결과를 수동으로 처리하고 있다면 isError를 확인하십시오.
사용 시점 (및 사용하지 말아야 할 시점)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기