메인 콘텐츠로 건너뛰기

MCP 서버란(고급 해설)

Model Context Protocol(MCP) 은 AI 클라이언트(Claude, Cursor, GitHub Copilot 등)와 애플리케이션이 표준화된 프로토콜로 통신하기 위한 사양입니다. MCP에는 3가지 주요 프리미티브가 있습니다.
프리미티브역할주요 용도
도구(Tools)AI가 호출할 수 있는 함수데이터 조작, 외부 API 연동, 계산 처리
리소스(Resources)AI가 읽을 수 있는 컨텍스트문서, 설정 정보, 동적 데이터
프롬프트(Prompts)재사용 가능한 템플릿정형 쿼리, 워크플로 유도
Laravel에서 MCP 서버를 구축하는 이점은 Eloquent, 캐시, 인증, 유효성 검증과 같은 Laravel 생태계를 그대로 활용할 수 있다는 점입니다.
이 고급 가이드에서는 실전 구현에 초점을 맞춥니다. MCP의 기본 개념은 중급: Laravel MCP를 참조하세요.

설치와 초기 설정

1

패키지 설치

Composer로 패키지를 설치합니다.
composer require laravel/mcp
2

라우트 파일 공개

vendor:publish로 MCP 서버 등록 위치가 되는 routes/ai.php를 생성합니다.
php artisan vendor:publish --tag=ai-routes
3

서버 클래스 생성

Artisan 명령어로 서버 클래스를 생성합니다.
php artisan make:mcp-server DatabaseServer
생성된 app/Mcp/Servers/DatabaseServer.php에 도구, 리소스, 프롬프트를 등록합니다.
4

서버 등록

routes/ai.php에서 서버를 라우트에 등록합니다.
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/database', DatabaseServer::class);
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('database', DatabaseServer::class);
Web 서버는 HTTP POST를 통해 접근합니다. 로컬 서버는 Artisan 명령어로 동작하며 CLI 기반 AI 클라이언트와의 연동에 사용됩니다.

도구 구현

도구는 AI 클라이언트가 호출할 수 있는 함수입니다. Laravel의 서비스 컨테이너, 유효성 검증, Eloquent를 그대로 사용할 수 있습니다.

도구 생성

php artisan make:mcp-tool SearchUsersTool
생성된 클래스에 handle 메서드와 schema 메서드를 구현합니다.

파라미터 정의(스키마)

schema 메서드에서 Illuminate\Contracts\JsonSchema\JsonSchema 빌더를 사용해 받아들일 파라미터를 정의합니다.
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('이름 또는 이메일 주소로 사용자를 검색합니다.')]
class SearchUsersTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'query'   => 'required|string|max:100',
            'limit'   => 'integer|min:1|max:50',
            'role'    => 'nullable|string|in:admin,editor,viewer',
        ], [
            'query.required' => '검색 키워드를 지정해 주세요. 예: "김철수" 또는 "[email protected]"',
            'limit.max'      => '조회 건수는 최대 50건까지 지정할 수 있습니다.',
            'role.in'        => '역할은 admin, editor, viewer 중 하나를 지정해 주세요.',
        ]);

        $users = \App\Models\User::query()
            ->where(function ($q) use ($validated) {
                $q->where('name', 'like', "%{$validated['query']}%")
                  ->orWhere('email', 'like', "%{$validated['query']}%");
            })
            ->when(isset($validated['role']), fn ($q) => $q->where('role', $validated['role']))
            ->limit($validated['limit'] ?? 10)
            ->get(['id', 'name', 'email', 'role']);

        if ($users->isEmpty()) {
            return Response::text('조건에 일치하는 사용자를 찾을 수 없습니다.');
        }

        $result = $users->map(fn ($u) => "ID:{$u->id} {$u->name} <{$u->email}> [{$u->role}]")
                        ->implode("\n");

        return Response::text($result);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'query' => $schema->string()
                ->description('검색 키워드. 이름 또는 이메일 주소로 검색합니다.')
                ->required(),

            'limit' => $schema->integer()
                ->description('조회 건수 상한(기본값: 10, 최대: 50).')
                ->minimum(1)
                ->maximum(50)
                ->default(10),

            'role' => $schema->string()
                ->description('필터링할 역할. admin, editor, viewer 중 하나.')
                ->enum(['admin', 'editor', 'viewer']),
        ];
    }
}

도구 어노테이션

MCP 프로토콜의 어노테이션을 사용하면 AI 클라이언트가 도구의 안전성을 판단할 수 있습니다.
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[IsReadOnly]      // 환경을 변경하지 않는 읽기 전용 도구
#[IsIdempotent]    // 동일한 인자로 여러 번 호출해도 결과가 변하지 않음
class SearchUsersTool extends Tool
{
    // ...
}
어노테이션설명
#[IsReadOnly]데이터를 변경하지 않음
#[IsDestructive]삭제 등 파괴적인 작업을 수행
#[IsIdempotent]동일한 인자에서의 재실행이 안전
#[IsOpenWorld]외부 엔티티와 통신

구조화된 응답

AI 클라이언트가 파싱하기 쉬운 JSON 형식의 응답을 반환하려면 Response::structured를 사용합니다.
return Response::structured([
    'total' => $users->count(),
    'users' => $users->map(fn ($u) => [
        'id'    => $u->id,
        'name'  => $u->name,
        'email' => $u->email,
    ])->toArray(),
]);

스트리밍 응답

장시간이 걸리는 처리에서는 Generator를 반환하여 도중 진행 상황을 스트리밍할 수 있습니다.
use Generator;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;

public function handle(Request $request): Generator
{
    $items = \App\Models\Product::all();
    $total = $items->count();

    foreach ($items as $index => $item) {
        yield Response::notification('processing/progress', [
            'current' => $index + 1,
            'total'   => $total,
            'name'    => $item->name,
        ]);

        // 무거운 처리...
        yield Response::text("처리 완료: {$item->name}");
    }
}
Web 서버에서는 스트리밍 응답이 자동으로 SSE(Server-Sent Events) 스트림으로 전송됩니다.

조건부 등록

특정 조건을 충족하는 사용자에게만 도구를 공개할 수 있습니다.
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->hasRole('admin') ?? false;
}

리소스 구현

리소스는 AI 클라이언트가 컨텍스트로 읽어 오는 데이터입니다. 문서, 설정 정보, 동적 데이터를 제공합니다.

정적 리소스

php artisan make:mcp-resource AppDocumentationResource
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('app://resources/documentation')]
#[MimeType('text/markdown')]
#[Description('애플리케이션의 API 문서와 비즈니스 규칙 개요.')]
class AppDocumentationResource extends Resource
{
    public function handle(Request $request): Response
    {
        $content = \Illuminate\Support\Facades\Storage::get('docs/api-overview.md')
            ?? '문서를 찾을 수 없습니다.';

        return Response::text($content);
    }
}

동적 리소스(URI 템플릿)

URI 템플릿을 사용하면 URL의 파라미터를 기반으로 동적인 리소스를 제공할 수 있습니다.
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Support\UriTemplate;

#[MimeType('application/json')]
#[Description('사용자 ID를 지정하여 사용자 정보를 가져옵니다.')]
class UserProfileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('app://users/{userId}/profile');
    }

    public function handle(Request $request): Response
    {
        $userId = $request->get('userId');

        $user = \App\Models\User::find($userId);

        if (! $user) {
            return Response::error("사용자 ID {$userId}를 찾을 수 없습니다.");
        }

        return Response::text(json_encode([
            'id'         => $user->id,
            'name'       => $user->name,
            'email'      => $user->email,
            'created_at' => $user->created_at->toIso8601String(),
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
    }
}
AI 클라이언트는 app://users/42/profile과 같은 URI로 리소스를 요청하며, {userId}의 값은 $request->get('userId')로 가져올 수 있습니다.

리소스 어노테이션

리소스의 우선순위나 대상 오디언스를 명시할 수 있습니다.
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\Priority;

#[Audience(Role::Assistant)]   // AI 어시스턴트 대상
#[Priority(0.8)]               // 중요도(0.0~1.0)
class AppDocumentationResource extends Resource
{
    // ...
}

프롬프트 구현

프롬프트는 AI 클라이언트가 사용할 수 있는 재사용 가능한 템플릿입니다. 정형적인 쿼리나 복잡한 워크플로를 표준화합니다.

프롬프트 생성

php artisan make:mcp-prompt DataAnalysisPrompt
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

#[Description('데이터 분석 보고서를 생성하기 위한 프롬프트 템플릿.')]
class DataAnalysisPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'target',
                description: '분석 대상. 예: "지난달 매출", "사용자 등록 수 추이"',
                required: true,
            ),
            new Argument(
                name: 'format',
                description: '출력 형식. "summary"(요약) 또는 "detail"(상세)',
                required: false,
            ),
        ];
    }

    public function handle(Request $request): array
    {
        $validated = $request->validate([
            'target' => 'required|string|max:200',
            'format' => 'nullable|in:summary,detail',
        ]);

        $target = $validated['target'];
        $format = $validated['format'] ?? 'summary';
        $instruction = $format === 'detail'
            ? '수치, 트렌드, 이상치, 권장 조치를 포함한 상세한 보고서를 작성해 주세요.'
            : '주요 지표와 3가지 중요한 통찰을 포함한 간결한 요약을 작성해 주세요.';

        return [
            Response::text(
                "당신은 데이터 분석가입니다. {$instruction}"
            )->asAssistant(),
            Response::text(
                "다음 데이터를 분석해 주세요: {$target}"
            ),
        ];
    }
}
asAssistant()를 사용하면 메시지가 AI 어시스턴트의 발언으로 취급됩니다. 시스템 프롬프트와 사용자 메시지를 조합하여 AI의 동작을 세밀하게 제어할 수 있습니다.

인증과 인가

Sanctum을 이용한 토큰 인증

가장 간단한 인증 방식입니다. MCP 클라이언트는 Authorization: Bearer <token> 헤더를 부여합니다.
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');

OAuth 2.1을 이용한 인증

보다 견고한 인증을 위해서는 Laravel Passport를 사용합니다.
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:api');
OAuth 인증을 사용할 경우, MCP에서 제공하는 인가 뷰를 공개하고 AppServiceProvider에 설정합니다.
php artisan vendor:publish --tag=mcp-views
// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;

public function boot(): void
{
    Passport::authorizationView(function ($parameters) {
        return view('mcp.authorize', $parameters);
    });
}

커스텀 미들웨어를 이용한 인증

자체 API 토큰을 사용하는 경우, 커스텀 미들웨어로 Authorization 헤더를 검증합니다.
// app/Http/Middleware/McpTokenMiddleware.php
public function handle($request, Closure $next)
{
    $token = $request->bearerToken();

    if (! $token || ! \App\Models\ApiToken::where('token', hash('sha256', $token))->exists()) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $next($request);
}

도구 내부에서의 인가

도구나 리소스의 handle 메서드 내부에서 $request->user()를 사용하여 세밀한 인가 검사를 할 수 있습니다.
public function handle(Request $request): Response
{
    $user = $request->user();

    if (! $user?->can('manage-users')) {
        return Response::error('이 도구를 사용할 권한이 없습니다. 관리자에게 문의하세요.');
    }

    // 인가된 처리를 계속...
}
shouldRegister는 도구를 목록에서 숨기기만 합니다. 도구가 호출될 때의 인가 검사는 반드시 handle 메서드 내부에서 수행하세요.

실전 예제: 데이터베이스 조작 도구

Eloquent를 사용해 데이터를 검색·생성하는 도구의 전체 구현 예제입니다.

서버 클래스

<?php

namespace App\Mcp\Servers;

use App\Mcp\Tools\CreatePostTool;
use App\Mcp\Tools\SearchPostsTool;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
use Laravel\Mcp\Server;

#[Name('Blog Management Server')]
#[Version('1.0.0')]
#[Instructions('블로그 게시글을 검색·생성할 수 있는 MCP 서버입니다.')]
class BlogServer extends Server
{
    protected array $tools = [
        SearchPostsTool::class,
        CreatePostTool::class,
    ];
}

검색 도구(읽기 전용)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('블로그 게시글을 키워드나 상태로 검색합니다.')]
#[IsReadOnly]
#[IsIdempotent]
class SearchPostsTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'keyword' => 'nullable|string|max:100',
            'status'  => 'nullable|in:draft,published,archived',
            'limit'   => 'integer|min:1|max:20',
        ]);

        $posts = Post::query()
            ->when($validated['keyword'] ?? null, fn ($q, $kw) =>
                $q->where('title', 'like', "%{$kw}%")
                  ->orWhere('body', 'like', "%{$kw}%")
            )
            ->when($validated['status'] ?? null, fn ($q, $s) => $q->where('status', $s))
            ->latest()
            ->limit($validated['limit'] ?? 5)
            ->get(['id', 'title', 'status', 'published_at']);

        if ($posts->isEmpty()) {
            return Response::text('조건에 일치하는 게시글을 찾을 수 없습니다.');
        }

        return Response::structured([
            'total' => $posts->count(),
            'posts' => $posts->map(fn ($p) => [
                'id'           => $p->id,
                'title'        => $p->title,
                'status'       => $p->status,
                'published_at' => $p->published_at?->toIso8601String(),
            ])->toArray(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'keyword' => $schema->string()
                ->description('제목 또는 본문에 포함된 키워드.'),

            'status' => $schema->string()
                ->description('게시글의 상태.')
                ->enum(['draft', 'published', 'archived']),

            'limit' => $schema->integer()
                ->description('조회 건수 상한(기본값: 5, 최대: 20).')
                ->default(5),
        ];
    }
}

생성 도구(쓰기)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('새 블로그 게시글을 초안으로 생성합니다.')]
class CreatePostTool extends Tool
{
    public function handle(Request $request): Response
    {
        $user = $request->user();

        if (! $user?->can('create-posts')) {
            return Response::error('게시글을 생성할 권한이 없습니다.');
        }

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
            'tags'  => 'nullable|array',
            'tags.*' => 'string|max:50',
        ], [
            'title.required' => '게시글 제목을 지정해 주세요.',
            'body.required'  => '게시글 본문을 지정해 주세요.',
        ]);

        $post = Post::create([
            'title'   => $validated['title'],
            'body'    => $validated['body'],
            'status'  => 'draft',
            'user_id' => $user->id,
        ]);

        if (! empty($validated['tags'])) {
            $post->syncTags($validated['tags']);
        }

        return Response::structured([
            'id'         => $post->id,
            'title'      => $post->title,
            'status'     => $post->status,
            'created_at' => $post->created_at->toIso8601String(),
            'message'    => '게시글을 초안으로 생성했습니다.',
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()
                ->description('게시글 제목(최대 255자).')
                ->required(),

            'body' => $schema->string()
                ->description('게시글 본문. Markdown을 사용할 수 있습니다.')
                ->required(),

            'tags' => $schema->array()
                ->description('게시글에 붙일 태그 배열. 예: ["Laravel", "PHP"]'),
        ];
    }
}

실전 예제: 파일 시스템 조작 도구

Storage 파사드를 사용하여 파일을 조작하는 도구의 구현 예제입니다.
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('스토리지 내 파일 목록을 조회하고, 텍스트 파일의 내용을 읽습니다.')]
#[IsReadOnly]
class ReadFileTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'path'    => 'required|string|max:500',
            'disk'    => 'nullable|string|in:local,public,s3',
        ]);

        $disk = $validated['disk'] ?? 'local';
        $path = $validated['path'];

        // 디렉터리 트래버설 공격 방지
        if (str_contains($path, '..')) {
            return Response::error('유효하지 않은 경로가 지정되었습니다.');
        }

        if (! Storage::disk($disk)->exists($path)) {
            return Response::error("파일을 찾을 수 없습니다: {$path}");
        }

        $mimeType = Storage::disk($disk)->mimeType($path);

        // 텍스트 파일만 내용을 반환
        if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') {
            $content = Storage::disk($disk)->get($path);
            return Response::text($content);
        }

        // 이미지 파일은 바이너리로 반환
        if (str_starts_with($mimeType, 'image/')) {
            return Response::fromStorage($path, disk: $disk);
        }

        return Response::error("이 파일 형식은 지원하지 않습니다: {$mimeType}");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()
                ->description('읽을 파일의 경로. 예: "reports/2025-01.csv"')
                ->required(),

            'disk' => $schema->string()
                ->description('사용할 스토리지 디스크.')
                ->enum(['local', 'public', 's3'])
                ->default('local'),
        ];
    }
}
파일 조작 도구에서는 반드시 경로 새니타이즈를 수행하여, 허용된 디렉터리 이외로의 접근을 차단하세요. ..를 포함하는 경로는 거부하는 것이 중요합니다.

테스트

MCP 서버, 도구, 리소스, 프롬프트는 Laravel 표준 테스트 기능으로 유닛 테스트를 작성할 수 있습니다.

도구 테스트

Server::tool() 메서드로 도구를 직접 호출하여 테스트합니다.
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

test('게시글을 검색할 수 있다', function () {
    Post::factory()->create(['title' => 'Laravel의 기초', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP의 응용', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Laravel의 기초');
});

test('권한이 없는 사용자는 게시글을 생성할 수 없다', function () {
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => '테스트 게시글',
            'body'  => '본문',
        ]);

    $response->assertSee('게시글을 생성할 권한이 없습니다');
});
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

public function test_게시글을_검색할_수_있다(): void
{
    Post::factory()->create(['title' => 'Laravel의 기초', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP의 응용', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Laravel의 기초');
}

public function test_권한이_없는_사용자는_게시글을_생성할_수_없다(): void
{
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => '테스트 게시글',
            'body'  => '본문',
        ]);

    $response->assertSee('게시글을 생성할 권한이 없습니다');
}

리소스와 프롬프트 테스트

// 리소스 테스트
$response = BlogServer::resource(\App\Mcp\Resources\AppDocumentationResource::class);
$response->assertOk()->assertSee('API');

// 프롬프트 테스트
$response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [
    'target' => '지난달 매출',
    'format' => 'summary',
]);
$response->assertOk()->assertSee('데이터 분석가');

주요 어설션 메서드

메서드설명
assertOk()응답에 오류가 없는지 확인
assertSee($text)응답에 지정된 텍스트가 포함되어 있는지 확인
assertHasErrors()응답에 오류가 있는지 확인
assertHasNoErrors()응답에 오류가 없는지 확인
assertName($name)도구 이름을 확인
assertSentNotification($method, $data)알림이 전송되었는지 확인
assertNotificationCount($count)알림 전송 횟수를 확인

MCP Inspector를 이용한 디버깅

인터랙티브한 디버깅에는 MCP Inspector를 사용합니다.
# Web 서버 검사
php artisan mcp:inspector mcp/database

# 로컬 서버 검사
php artisan mcp:inspector database

배포 시 고려 사항

HTTP 스트리밍과 SSE

Web 서버에서 스트리밍 응답(Generator)을 사용하는 경우, 서버 설정을 확인하세요.
location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # SSE에서는 버퍼링을 비활성화
    proxy_cache off;
    proxy_read_timeout 3600s;     # 장시간 연결에 대응
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
}
# mod_proxy_http를 사용하는 경우
ProxyPass /mcp http://127.0.0.1:8000/mcp
ProxyPassReverse /mcp http://127.0.0.1:8000/mcp
SetEnv proxy-sendchunks 1

Laravel Octane과의 조합

트래픽이 많은 MCP 서버에서는 Laravel Octane(FrankenPHP 또는 Swoole) 사용을 검토하세요. 요청당 오버헤드가 크게 줄어듭니다.
Octane을 사용할 때는 요청 간에 상태가 공유됩니다. 도구 내부에서 정적 프로퍼티나 전역 상태를 사용하지 않도록 주의하세요.

레이트 리밋

throttle 미들웨어로 MCP 서버에 대한 요청을 제한합니다.
// config/cache.php에서 전용 레이트 리미터를 정의
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

캐시

자주 호출되는 읽기 전용 도구에는 캐시를 활용합니다.
public function handle(Request $request): Response
{
    $data = \Illuminate\Support\Facades\Cache::remember(
        "mcp:search:{$request->get('keyword')}",
        now()->addMinutes(5),
        fn () => $this->fetchFromDatabase($request)
    );

    return Response::structured($data);
}

로그와 모니터링

MCP 도구 호출을 로그에 기록하면 AI 클라이언트의 이용 상황을 파악할 수 있습니다.
public function handle(Request $request): Response
{
    \Illuminate\Support\Facades\Log::info('MCP tool called', [
        'tool'   => static::class,
        'user'   => $request->user()?->id,
        'params' => $request->all(),
    ]);

    // ...
}
프로덕션 환경에서는 Laravel Telescope나 Sentry를 사용하여 MCP 서버의 성능과 예외를 모니터링하는 것을 권장합니다.
마지막 수정일 2026년 7월 13일