> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel MCP

> Laravel 애플리케이션에 Model Context Protocol(MCP) 서버를 조합하는 방법을 배웁니다. AI 코딩 에이전트가 여러분의 애플리케이션과 대화할 수 있는 도구, 리소스, 프롬프트를 정의할 수 있습니다.

## MCP 란

**Model Context Protocol(MCP)** 은 AI 클라이언트(Claude, Cursor, GitHub Copilot 등)와 애플리케이션이 표준화된 프로토콜로 통신하기 위한 사양입니다. MCP 서버를 구현함으로써 AI 에이전트는 여러분의 Laravel 애플리케이션의 데이터에 접근하거나 액션을 실행할 수 있게 됩니다.

<Info>
  Laravel MCP는 Laravel 13에서 추가된 공식 패키지입니다. `laravel/mcp`로 제공되며, MCP 서버의 구축에 필요한 일련의 기능을 제공합니다.
</Info>

MCP 서버가 제공할 수 있는 기능은 주로 3가지입니다.

| 기능                 | 설명                                         |
| ------------------ | ------------------------------------------ |
| **도구(Tools)**      | AI 클라이언트가 호출할 수 있는 함수. 검색·업데이트·외부 API 연계 등 |
| **리소스(Resources)** | AI 클라이언트가 읽어 들일 수 있는 데이터나 컨텍스트 정보          |
| **프롬프트(Prompts)**  | 재사용 가능한 프롬프트 템플릿                           |

## 설치

Composer로 패키지를 설치합니다.

```shell theme={null}
composer require laravel/mcp
```

설치 후 `vendor:publish` Artisan 명령어를 실행해 `routes/ai.php` 파일을 생성합니다.

```shell theme={null}
php artisan vendor:publish --tag=ai-routes
```

이 명령어에 의해 `routes/ai.php` 파일이 작성됩니다. 여기에 MCP 서버의 등록을 기술합니다.

## 서버의 작성

`make:mcp-server` Artisan 명령어로 서버 클래스를 생성합니다.

```shell theme={null}
php artisan make:mcp-server WeatherServer
```

`app/Mcp/Servers` 디렉터리에 서버 클래스가 생성됩니다.

```php theme={null}
<?php

namespace App\Mcp\Servers;

use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
use Laravel\Mcp\Server;

#[Name('Weather Server')]
#[Version('1.0.0')]
#[Instructions('This server provides weather information and forecasts.')]
class WeatherServer extends Server
{
    protected array $tools = [
        // GetCurrentWeatherTool::class,
    ];

    protected array $resources = [
        // WeatherGuidelinesResource::class,
    ];

    protected array $prompts = [
        // DescribeWeatherPrompt::class,
    ];
}
```

### 서버의 등록

서버를 작성했다면 `routes/ai.php`에서 등록합니다. 등록 방법에는 **Web 서버**와 **로컬 서버**의 2종류가 있습니다.

#### Web 서버

Web 서버는 HTTP POST 요청으로 접근할 수 있습니다. 원격 AI 클라이언트나 Web 기반의 연계에 최적입니다.

```php theme={null}
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/weather', WeatherServer::class);
```

일반 라우트와 동일하게 미들웨어를 적용할 수 있습니다.

```php theme={null}
Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware(['throttle:mcp']);
```

#### 로컬 서버

로컬 서버는 Artisan 명령어로서 동작합니다. Claude Desktop 등의 로컬 AI 클라이언트와의 연계에 사용합니다.

```php theme={null}
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('weather', WeatherServer::class);
```

<Tip>
  로컬 서버는 일반적으로 MCP 클라이언트가 자동으로 기동합니다. 수동으로 `mcp:start` Artisan 명령어를 실행할 필요는 없습니다.
</Tip>

## 도구

도구는 AI 클라이언트가 호출할 수 있는 함수입니다. 데이터의 취득, 외부 API와의 연계, 데이터베이스의 조작 등을 구현할 수 있습니다.

### 도구의 작성

`make:mcp-tool` Artisan 명령어로 도구 클래스를 생성합니다.

```shell theme={null}
php artisan make:mcp-tool CurrentWeatherTool
```

작성한 도구를 서버의 `$tools` 프로퍼티에 등록합니다.

```php theme={null}
use App\Mcp\Tools\CurrentWeatherTool;
use Laravel\Mcp\Server;

class WeatherServer extends Server
{
    protected array $tools = [
        CurrentWeatherTool::class,
    ];
}
```

기본적인 도구 클래스의 구현 예시입니다.

```php theme={null}
<?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('Fetches the current weather forecast for a specified location.')]
class CurrentWeatherTool extends Tool
{
    public function handle(Request $request): Response
    {
        $location = $request->get('location');

        // 날씨 데이터를 취득...

        return Response::text('The weather is sunny, 22°C.');
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'location' => $schema->string()
                ->description('The location to get the weather for.')
                ->required(),
        ];
    }
}
```

### 도구의 이름과 설명

클래스 이름에서 기본 이름과 타이틀이 자동 생성됩니다. `CurrentWeatherTool`이라면 이름은 `current-weather`, 타이틀은 `Current Weather Tool`이 됩니다. `Name`과 `Title` 속성으로 커스터마이즈할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Title;

#[Name('get-optimistic-weather')]
#[Title('Get Optimistic Weather Forecast')]
class CurrentWeatherTool extends Tool
{
    // ...
}
```

<Warning>
  도구의 설명(`Description`)은 자동 생성되지 않습니다. AI 모델이 도구의 사용법을 이해하기 위해 필수이므로, 반드시 의미 있는 설명을 설정해 주십시오.
</Warning>

### 입력 스키마

`schema` 메서드로 입력 파라미터의 스키마를 정의합니다. Laravel의 JSON 스키마 빌더를 사용해 타입이나 제약을 지정할 수 있습니다.

```php theme={null}
public function schema(JsonSchema $schema): array
{
    return [
        'location' => $schema->string()
            ->description('The location to get the weather for.')
            ->required(),

        'units' => $schema->string()
            ->enum(['celsius', 'fahrenheit'])
            ->description('The temperature units to use.')
            ->default('celsius'),
    ];
}
```

### 출력 스키마

`outputSchema` 메서드로 응답의 구조를 정의할 수 있습니다. AI 클라이언트가 응답을 해석하기 쉬워집니다.

```php theme={null}
public function outputSchema(JsonSchema $schema): array
{
    return [
        'temperature' => $schema->number()
            ->description('Temperature in Celsius')
            ->required(),

        'conditions' => $schema->string()
            ->description('Weather conditions')
            ->required(),

        'humidity' => $schema->integer()
            ->description('Humidity percentage')
            ->required(),
    ];
}
```

### 밸리데이션

`handle` 메서드 내에서 Laravel의 표준 밸리데이션 기능을 사용할 수 있습니다.

```php theme={null}
public function handle(Request $request): Response
{
    $validated = $request->validate([
        'location' => 'required|string|max:100',
        'units' => 'in:celsius,fahrenheit',
    ], [
        'location.required' => 'You must specify a location. For example, "New York City" or "Tokyo".',
        'units.in' => 'You must specify either "celsius" or "fahrenheit" for the units.',
    ]);

    // 밸리데이션 완료된 데이터를 사용해 처리...
}
```

<Tip>
  밸리데이션 실패 시에 AI 클라이언트는 에러 메시지를 참고해 재시도합니다. 구체적이고 실행 가능한 에러 메시지를 제공해 주십시오.
</Tip>

### 의존성 주입

Laravel의 서비스 컨테이너를 통해 도구가 해결되므로, 생성자나 `handle` 메서드에서 의존 관계를 타입 힌트할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Tools;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;

class CurrentWeatherTool extends Tool
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}

    public function handle(Request $request, WeatherRepository $weather): Response
    {
        $location = $request->get('location');
        $forecast = $weather->getForecastFor($location);

        return Response::text("Forecast: {$forecast}");
    }
}
```

### 어노테이션

도구에 어노테이션을 추가함으로써 AI 클라이언트에 도구의 거동에 관한 추가 정보를 제공할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[IsIdempotent]
#[IsReadOnly]
class CurrentWeatherTool extends Tool
{
    // ...
}
```

이용 가능한 어노테이션은 다음과 같습니다.

| 어노테이션              | 설명                            |
| ------------------ | ----------------------------- |
| `#[IsReadOnly]`    | 도구가 환경을 변경하지 않음을 나타냄          |
| `#[IsDestructive]` | 도구가 파괴적인 업데이트를 할 가능성이 있음을 나타냄 |
| `#[IsIdempotent]`  | 같은 인수로 반복 호출해도 부작용이 없음을 나타냄   |
| `#[IsOpenWorld]`   | 도구가 외부 엔티티와 대화할 가능성이 있음을 나타냄  |

### 조건부 등록

`shouldRegister` 메서드를 구현함으로써 실행 시에 도구를 조건부로 등록할 수 있습니다.

```php theme={null}
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->subscribed() ?? false;
}
```

`false`를 반환하면 그 도구는 AI 클라이언트로부터 보이지 않게 됩니다.

### 응답

도구는 `Laravel\Mcp\Response`의 인스턴스를 반환해야 합니다.

<AccordionGroup>
  <Accordion title="텍스트 응답">
    ```php theme={null}
    return Response::text('Weather Summary: Sunny, 22°C');
    ```
  </Accordion>

  <Accordion title="에러 응답">
    ```php theme={null}
    return Response::error('Unable to fetch weather data. Please try again.');
    ```
  </Accordion>

  <Accordion title="이미지·음성 응답">
    ```php theme={null}
    return Response::image(file_get_contents(storage_path('weather/radar.png')), 'image/png');

    return Response::audio(file_get_contents(storage_path('weather/alert.mp3')), 'audio/mp3');

    // 스토리지에서 직접 읽어 들임(MIME 타입은 자동 검출)
    return Response::fromStorage('weather/radar.png');
    ```
  </Accordion>

  <Accordion title="여러 콘텐츠의 응답">
    ```php theme={null}
    public function handle(Request $request): array
    {
        return [
            Response::text('Weather Summary: Sunny, 22°C'),
            Response::text("**Detailed Forecast**\n- Morning: 18°C\n- Afternoon: 25°C"),
        ];
    }
    ```
  </Accordion>

  <Accordion title="구조화 응답">
    AI 클라이언트가 해석하기 쉬운 구조화 데이터를 반환합니다.

    ```php theme={null}
    return Response::structured([
        'temperature' => 22.5,
        'conditions' => 'Partly cloudy',
        'humidity' => 65,
    ]);
    ```
  </Accordion>

  <Accordion title="스트리밍 응답">
    장시간 걸리는 처리로 도중 경과를 실시간 전송합니다.

    ```php theme={null}
    public function handle(Request $request): Generator
    {
        $locations = $request->array('locations');

        foreach ($locations as $index => $location) {
            yield Response::notification('processing/progress', [
                'current' => $index + 1,
                'total' => count($locations),
                'location' => $location,
            ]);

            yield Response::text($this->forecastFor($location));
        }
    }
    ```
  </Accordion>
</AccordionGroup>

## 프롬프트

프롬프트는 재사용 가능한 프롬프트 템플릿입니다. AI 클라이언트가 언어 모델과 대화할 때 사용하는 정형적인 쿼리를 표준화된 형태로 제공할 수 있습니다.

### 프롬프트의 작성

```shell theme={null}
php artisan make:mcp-prompt DescribeWeatherPrompt
```

서버의 `$prompts` 프로퍼티에 등록합니다.

```php theme={null}
use App\Mcp\Prompts\DescribeWeatherPrompt;

class WeatherServer extends Server
{
    protected array $prompts = [
        DescribeWeatherPrompt::class,
    ];
}
```

### 프롬프트의 인수

`arguments` 메서드로 프롬프트의 파라미터를 정의합니다.

```php theme={null}
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

class DescribeWeatherPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'tone',
                description: 'The tone to use in the weather description (e.g., formal, casual, humorous).',
                required: true,
            ),
        ];
    }
}
```

### 밸리데이션

프롬프트의 인수는 정의에 기반해 자동으로 밸리데이션되지만, 더 복잡한 밸리데이션 룰을 적용할 수도 있습니다.

Laravel MCP는 Laravel의 [밸리데이션 기능](/ko/validation)과 매끄럽게 연계합니다. 프롬프트의 `handle` 메서드 내에서 인수를 밸리데이션할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'tone' => 'required|string|max:50',
        ]);

        $tone = $validated['tone'];

        // 지정된 tone 을 사용해 프롬프트를 생성...
    }
}
```

밸리데이션 실패 시, AI 클라이언트는 에러 메시지를 참고해 재시도합니다. 구체적이고 실행 가능한 메시지를 제공해 주십시오.

```php theme={null}
$validated = $request->validate([
    'tone' => ['required', 'string', 'max:50'],
], [
    'tone.*' => 'tone 을 지정해 주십시오. 예: "formal", "casual", "humorous".',
]);
```

### 의존성 주입

Laravel의 서비스 컨테이너를 통해 프롬프트가 해결되므로, 생성자나 `handle` 메서드에서 의존 관계를 타입 힌트할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Prompts;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}
}
```

`handle` 메서드에서도 타입 힌트할 수 있으며, 서비스 컨테이너가 자동으로 해결·주입합니다.

```php theme={null}
public function handle(Request $request, WeatherRepository $weather): Response
{
    $isAvailable = $weather->isServiceAvailable();

    // ...
}
```

### 조건부 등록

`shouldRegister` 메서드를 구현함으로써 실행 시에 프롬프트를 조건부로 등록할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Prompt;

class CurrentWeatherPrompt extends Prompt
{
    public function shouldRegister(Request $request): bool
    {
        return $request?->user()?->subscribed() ?? false;
    }
}
```

`false`를 반환하면 그 프롬프트는 AI 클라이언트로부터 보이지 않게 되며, 호출도 할 수 없게 됩니다.

### 프롬프트의 응답

프롬프트의 `handle` 메서드에서는 사용자 메시지와 어시스턴트 메시지를 반환할 수 있습니다. `asAssistant()`로 어시스턴트 측 메시지로서 취급합니다.

```php theme={null}
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;

class DescribeWeatherPrompt extends Prompt
{
    public function handle(Request $request): array
    {
        $tone = $request->string('tone');

        $systemMessage = "You are a helpful weather assistant. Please provide a weather description in a {$tone} tone.";
        $userMessage = 'What is the current weather like in Tokyo?';

        return [
            Response::text($systemMessage)->asAssistant(),
            Response::text($userMessage),
        ];
    }
}
```

## 리소스

리소스는 AI 클라이언트가 컨텍스트로서 읽어 들일 수 있는 데이터나 정보입니다. 문서, 설정 정보, 동적 데이터 등, AI의 응답 품질을 향상시키는 정보를 제공할 수 있습니다.

### 리소스의 작성

```shell theme={null}
php artisan make:mcp-resource WeatherGuidelinesResource
```

서버의 `$resources` 프로퍼티에 등록합니다.

```php theme={null}
use App\Mcp\Resources\WeatherGuidelinesResource;

class WeatherServer extends Server
{
    protected array $resources = [
        WeatherGuidelinesResource::class,
    ];
}
```

```php theme={null}
<?php

namespace App\Mcp\Resources;

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

#[Description('Comprehensive guidelines for using the Weather API.')]
class WeatherGuidelinesResource extends Resource
{
    public function handle(Request $request): Response
    {
        $guidelines = "# Weather API Guidelines\n\n- Always specify a location...";

        return Response::text($guidelines);
    }
}
```

### URI 와 MIME 타입

기본적으로 클래스 이름에서 URI가 자동 생성됩니다(예: `weather://resources/weather-guidelines`). `Uri`와 `MimeType` 속성으로 커스터마이즈할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('weather://resources/guidelines')]
#[MimeType('application/pdf')]
class WeatherGuidelinesResource extends Resource
{
    // ...
}
```

### 리소스 템플릿

URI 변수를 가진 동적 리소스를 정의하려면 `HasUriTemplate` 인터페이스를 구현합니다.

```php theme={null}
<?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;

#[Description('Access user files by ID')]
#[MimeType('text/plain')]
class UserFileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('file://users/{userId}/files/{fileId}');
    }

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

        // 파일 콘텐츠를 취득해 반환...

        return Response::text("File {$fileId} for user {$userId}");
    }
}
```

URI로부터의 변수는 자동으로 요청에 도입되어, `get` 메서드로 취득할 수 있습니다.

### 리소스의 요청

도구나 프롬프트와 달리 리소스는 입력 스키마나 인수를 정의할 수 없습니다. 다만 `handle` 메서드 내에서 요청 객체를 통해 요청 정보에 접근할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function handle(Request $request): Response
    {
        // 요청 정보에 접근...
    }
}
```

### 리소스의 의존성 주입

Laravel의 서비스 컨테이너를 통해 리소스가 해결되므로, 생성자나 `handle` 메서드에서 의존 관계를 타입 힌트할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Resources;

use App\Repositories\WeatherRepository;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function __construct(
        protected WeatherRepository $weather,
    ) {}
}
```

`handle` 메서드에서도 타입 힌트할 수 있으며, 서비스 컨테이너가 자동으로 해결·주입합니다.

```php theme={null}
public function handle(WeatherRepository $weather): Response
{
    return Response::text($weather->guidelines());
}
```

### 리소스의 어노테이션

리소스에는 오디언스, 우선도, 최종 업데이트일 등의 어노테이션을 붙일 수 있습니다.

```php theme={null}
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\LastModified;
use Laravel\Mcp\Server\Annotations\Priority;
use Laravel\Mcp\Server\Resource;

#[Audience(Role::User)]
#[LastModified('2025-01-12T15:00:58Z')]
#[Priority(0.9)]
class UserDashboardResource extends Resource
{
    // ...
}
```

| 어노테이션             | 타입         | 설명                                              |
| ----------------- | ---------- | ----------------------------------------------- |
| `#[Audience]`     | Role 또는 배열 | 대상 오디언스(`Role::User`, `Role::Assistant`, 또는 양쪽) |
| `#[Priority]`     | float      | 중요도 스코어(0.0\~1.0)                               |
| `#[LastModified]` | string     | ISO 8601 형식의 최종 업데이트 일시                         |

### 리소스의 조건부 등록

`shouldRegister` 메서드를 구현함으로써 실행 시에 리소스를 조건부로 등록할 수 있습니다.

```php theme={null}
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Server\Resource;

class WeatherGuidelinesResource extends Resource
{
    public function shouldRegister(Request $request): bool
    {
        return $request?->user()?->subscribed() ?? false;
    }
}
```

`false`를 반환하면 그 리소스는 AI 클라이언트로부터 보이지 않게 되며, 접근도 할 수 없게 됩니다.

### 리소스의 응답

리소스는 `Laravel\Mcp\Response`의 인스턴스를 반환해야 합니다.

텍스트 콘텐츠에는 `text` 메서드를 사용합니다.

```php theme={null}
return Response::text($weatherData);
```

#### 리소스 링크 응답

`resourceLink` 메서드로 리소스 링크를 반환할 수 있습니다. 매입 리소스와 달리 AI 클라이언트가 독립적으로 취득하는 URI 포인터를 반환합니다.

```php theme={null}
return Response::resourceLink(
    uri: 'file:///data/report.json',
    name: 'monthly-report',
    mimeType: 'application/json',
);
```

등록된 리소스 클래스 또는 인스턴스를 전달할 수도 있습니다. URI, 이름, 타이틀, 설명, MIME 타입을 자동 상속합니다.

```php theme={null}
return Response::resourceLink(new WeatherForecastResource);
```

#### Blob 응답

바이너리 콘텐츠를 반환하려면 `blob` 메서드를 사용합니다. MIME 타입은 리소스의 `#[MimeType]` 속성으로 설정합니다.

```php theme={null}
return Response::blob(file_get_contents(storage_path('weather/radar.png')));
```

```php theme={null}
#[MimeType('image/png')]
class WeatherGuidelinesResource extends Resource
{
    // ...
}
```

#### 에러 응답

에러를 나타내는 경우 `error` 메서드를 사용합니다.

```php theme={null}
return Response::error('지정된 장소의 날씨 데이터를 취득할 수 없었습니다.');
```

## 앱

Laravel MCP는 [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview)를 지원합니다. 이것은 Model Context Protocol의 확장 기능으로, 도구가 지원된 호스트 내의 샌드박스 iframe 상에서 인터랙티브한 HTML 애플리케이션을 렌더링할 수 있게 합니다. 이에 따라 플레인 텍스트 응답을 넘어선 대시보드, 폼, 시각화, 그 외의 리치한 경험을 구축할 수 있습니다.

MCP 앱은 다음 2개의 파트가 연계되어 동작합니다.

* **앱 리소스** — 애플리케이션의 자기 완결형 HTML을 반환합니다.
* **도구** — `#[RendersApp]` 어트리뷰트를 사용해 앱 리소스에 링크됩니다. 도구가 호출되면 호스트가 링크된 리소스를 취득해 렌더링합니다.

### 앱 리소스의 작성

`make:mcp-app-resource` Artisan 명령어로 앱 리소스를 작성할 수 있습니다.

```shell theme={null}
php artisan make:mcp-app-resource WeatherDashboardApp
```

이 명령어는 2개의 파일을 작성합니다. `app/Mcp/Resources` 내의 PHP 클래스와 `resources/views/mcp` 내의 Blade 뷰입니다. 뷰 이름은 클래스 이름에서 자동으로 추측됩니다. 예를 들어 `WeatherDashboardApp`은 `mcp.weather-dashboard-app`으로 매핑됩니다.

```php theme={null}
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\AppMeta;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\AppResource;

#[Description('An interactive weather dashboard.')]
#[AppMeta]
class WeatherDashboardApp extends AppResource
{
    /**
     * Handle the app resource request.
     */
    public function handle(Request $request): Response
    {
        return Response::view('mcp.weather-dashboard-app', [
            'title' => $this->title(),
        ]);
    }
}
```

`AppResource`는 베이스의 `Resource` 클래스를 상속하며, MCP Apps 사양에서 요구되는 `ui://` URI 스킴과 `text/html;profile=mcp-app` MIME 타입을 자동으로 설정합니다. 다른 리소스와 마찬가지로 서버의 `$resources` 배열에 등록해야 합니다.

생성된 Blade 뷰는 `<x-mcp::app>` 컴포넌트를 사용합니다. 이 컴포넌트는 클라이언트 사이드의 MCP SDK가 번들된 완전한 HTML 문서를 렌더링합니다.

```blade theme={null}
<x-mcp::app :title="$title">
    <x-slot:head>
        <script type="module">
        createMcpApp(async (app) => {
            document.getElementById('run-btn').addEventListener('click', async () => {
                const result = await app.callServerTool('get-weather-data', {});
                document.getElementById('output').textContent = result.content[0]?.text ?? '';
            });
        });
        </script>
    </x-slot:head>

    <div id="app">
        <button id="run-btn">Refresh</button>
        <p id="output"></p>
    </div>
</x-mcp::app>
```

`createMcpApp` 글로벌 함수는 번들된 SDK가 제공합니다. iframe의 서버에의 접속, 호스트 테마의 적용, `callServerTool`·`sendMessage`·`openLink` 등의 헬퍼나 이벤트 콜백의 공개를 처리합니다. 완전한 클라이언트 사이드 API에 대해서는 [MCP Apps 사양](https://modelcontextprotocol.io/extensions/apps/overview)을 참조해 주십시오.

### 도구에서 앱을 렌더링

앱 리소스를 표시하려면 `#[RendersApp]` 어트리뷰트를 사용해 도구에 링크합니다. 도구가 호출되면 Laravel MCP는 리소스의 URI를 도구의 메타데이터에 포함시켜, 호스트가 샌드박스 iframe 내에서 앱을 렌더링할 수 있게 합니다.

```php theme={null}
<?php

namespace App\Mcp\Tools;

use App\Mcp\Resources\WeatherDashboardApp;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\RendersApp;
use Laravel\Mcp\Server\Tool;

#[RendersApp(resource: WeatherDashboardApp::class)]
class ShowWeatherDashboard extends Tool
{
    /**
     * Handle the tool request.
     */
    public function handle(Request $request): Response
    {
        return Response::text('Weather dashboard loaded.');
    }
}
```

<Info>
  `AppResource`가 등록되면 Laravel MCP는 자동으로 `io.modelcontextprotocol/ui` 케이퍼빌리티를 어드버타이즈합니다. 추가의 서버 설정은 불필요합니다.
</Info>

### 앱 도구의 가시성

각 `#[RendersApp]` 도구는 `visibility` 인수로 호출자를 제한할 수 있습니다. 이것은 UI가 데이터를 읽어 들이거나 업데이트하기 위해 호출하는 프라이빗한 앱 전용 도구를 모델로부터 보이지 않게 하는 경우에 편리합니다.

```php theme={null}
use Laravel\Mcp\Server\Attributes\RendersApp;
use Laravel\Mcp\Server\Ui\Enums\Visibility;

#[RendersApp(resource: WeatherDashboardApp::class, visibility: [Visibility::App])]
class GetWeatherData extends Tool
{
    // ...
}
```

`Visibility` enum에는 `Model`과 `App`의 2개의 케이스가 있으며, 기본은 양쪽입니다. UI가 직접 호출하는 백엔드 액션에는 `[Visibility::App]`을, UI에서 도구를 이용 불가능하게 하려면 `[Visibility::Model]`을 사용합니다.

### 앱의 설정

앱 리소스의 `#[AppMeta]` 어트리뷰트로 iframe의 Content Security Policy, 브라우저 권한, 뷰의 `<head>`에 포함시킬 라이브러리 스크립트를 설정합니다.

```php theme={null}
use Laravel\Mcp\Server\Attributes\AppMeta;
use Laravel\Mcp\Server\Ui\Enums\Library;
use Laravel\Mcp\Server\Ui\Enums\Permission;

#[AppMeta(
    connectDomains: ['https://api.weather.com'],
    permissions: [Permission::Geolocation],
    libraries: [Library::Tailwind, Library::Alpine],
)]
class WeatherDashboardApp extends AppResource
{
    // ...
}
```

`Library` enum에는 `Library::Tailwind`나 `Library::Alpine` 등 일반적인 프런트엔드 라이브러리의 CDN 스크립트가 사전 설정되어 있으며, CDN 오리진은 자동으로 CSP에 머지됩니다. `Permission` enum은 `Camera`·`Microphone`·`Geolocation`·`ClipboardWrite` 등의 브라우저 권한을 커버합니다.

<Tip>
  동적인 설정이 필요한 경우, `Laravel\Mcp\Server\Ui` 네임스페이스의 `AppMeta`·`Csp`·`Permissions` 플루언트 빌더를 사용해 리소스의 `appMeta` 메서드를 오버라이드합니다.
</Tip>

### Boost 를 사용한 앱 개발

Laravel MCP에는 MCP Apps를 구축하기 위한 전용 [Boost](/ko/boost) 스킬 레퍼런스가 포함되어 있습니다. Laravel Boost가 설치되어 있다면, AI 코딩 에이전트가 `mcp-development` 스킬을 호출해 앱 리소스, Blade 뷰, 링크된 도구를 자동 생성할 수 있습니다.

프로토콜의 완전한 레퍼런스(클라이언트 사이드 API나 스키마의 상세 포함)에 대해서는 공식의 [MCP Apps 문서](https://modelcontextprotocol.io/extensions/apps/overview)를 참조해 주십시오.

## 메타데이터

MCP 사양의 `_meta` 필드를 도구, 리소스, 프롬프트의 응답에 부가할 수 있습니다.

```php theme={null}
// 응답 콘텐츠에의 메타데이터
return Response::text('The weather is sunny.')
    ->withMeta(['source' => 'weather-api', 'cached' => true]);
```

응답 엔벨로프 전체에 메타데이터를 붙이는 경우는 `Response::make`를 사용합니다.

```php theme={null}
return Response::make(
    Response::text('The weather is sunny.')
)->withMeta(['request_id' => '12345']);
```

도구·리소스·프롬프트의 클래스 자체에 메타데이터를 붙이려면 `$meta` 프로퍼티를 정의합니다.

```php theme={null}
class CurrentWeatherTool extends Tool
{
    protected ?array $meta = [
        'version' => '2.0',
        'author' => 'Weather Team',
    ];
}
```

## 아이콘

MCP 클라이언트는 서버와 그 프리미티브의 아이콘을 표시할 수 있습니다. `Icon` 속성을 사용해 서버, 도구, 리소스, 프롬프트에 아이콘을 선언할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Enums\IconTheme;
use Laravel\Mcp\Server\Attributes\Icon;

#[Icon('mcp/server.png', mimeType: 'image/png', sizes: ['48x48'])]
#[Icon('mcp/server-dark.svg', theme: IconTheme::Dark)]
class WeatherServer extends Server
{
    // ...
}
```

`Icon` 속성은 반복 사용 가능하므로, 서로 다른 사이즈나 라이트·다크 테마의 배리언트를 제공하기 위해 여러 아이콘을 선언할 수 있습니다.

또는 `icons` 메서드를 오버라이드해 프로그램으로 아이콘을 정의할 수도 있습니다. 이것은 아이콘이 런타임의 조건에 의존하는 경우에 편리합니다.

```php theme={null}
use Laravel\Mcp\Schema\Icon;

class CurrentWeatherTool extends Tool
{
    /**
     * 도구의 아이콘을 취득합니다.
     *
     * @return array<int, Icon>
     */
    public function icons(): array
    {
        return [
            Icon::from('mcp/tool.png', mimeType: 'image/png'),
        ];
    }
}
```

속성과 `icons` 메서드로 정의된 아이콘은 자동으로 결합됩니다. 아이콘 경로는 다음과 같이 해결됩니다.

* `https:`나 `data:` 등의 URI 스킴을 갖는 경로는 그대로 사용됩니다.
* 상대 경로는 Laravel의 `asset` 헬퍼를 사용해 URL로 해결됩니다.

## 인증

Web 서버는 Laravel의 표준적인 미들웨어로 인증할 수 있습니다.

### Sanctum

[Laravel Sanctum](https://laravel.com/docs/sanctum)을 사용한 토큰 인증입니다. MCP 클라이언트는 `Authorization: Bearer <token>` 헤더를 전송합니다.

```php theme={null}
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware('auth:sanctum');
```

### OAuth 2.1

[Laravel Passport](https://laravel.com/docs/passport)를 사용한 OAuth 인증입니다. 더 견고한 보안이 필요한 경우에 적합합니다.

```php theme={null}
use App\Mcp\Servers\WeatherServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/weather', WeatherServer::class)
    ->middleware('auth:api');
```

OAuth 인증을 사용하는 경우, Passport의 인가 뷰를 공개해 서비스 프로바이더에 설정합니다.

```shell theme={null}
php artisan vendor:publish --tag=mcp-views
```

```php theme={null}
// AppServiceProvider::boot()
use Laravel\Passport\Passport;

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

## 인가

`$request->user()`로 인증된 사용자를 취득해, 도구나 리소스 내에서 인가 체크를 할 수 있습니다.

```php theme={null}
public function handle(Request $request): Response
{
    if (! $request->user()->can('read-weather')) {
        return Response::error('Permission denied.');
    }

    // 처리를 계속...
}
```

## MCP 클라이언트

Laravel MCP는 서버의 구축뿐 아니라, 다른 MCP 서버로 접속하기 위한 클라이언트도 제공하고 있습니다. 클라이언트를 사용함으로써 외부 MCP 서버가 공개하는 도구를 발견·호출할 수 있습니다. [AI 에이전트](/ko/ai-sdk#mcp-tools)에 외부 MCP 서버의 기능을 제공할 때 특히 유용합니다.

### 서버에의 접속

HTTP로 접근할 수 있는 MCP 서버에는 `Client::web` 메서드를 사용하고, 서버의 URL을 전달합니다.

```php theme={null}
use Laravel\Mcp\Client;

$client = Client::web('https://mcp.example.com');
```

명령어로서 기동하는 로컬 MCP 서버에는 `Client::local` 메서드를 사용하고, 명령어와 인수를 전달합니다.

```php theme={null}
use Laravel\Mcp\Client;

$client = Client::local('php', ['artisan', 'mcp:start']);
```

클라이언트는 지연 접속(lazy connect)하며, 도구의 목록 취득이나 호출을 처음 할 때 자동으로 접속을 확립합니다. 접속을 수동으로 관리하는 경우, `connect`, `connected`, `ping`, `disconnect` 메서드를 사용합니다.

```php theme={null}
$client->connect();

$client->ping();

if ($client->connected()) {
    // ...
}

$client->disconnect();
```

`withTimeout` 메서드로 요청 타임아웃을 커스터마이즈할 수 있습니다.

```php theme={null}
$client = Client::web('https://mcp.example.com')->withTimeout(30);
```

### 이름 있는 클라이언트

클라이언트를 매번 구축하는 대신, 재사용 가능한 이름 있는 클라이언트를 등록할 수 있습니다. 통상은 `Mcp` 파사드를 사용하고, 서비스 프로바이더의 `boot` 메서드 내에서 수행합니다.

```php theme={null}
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;

Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com'));
```

등록 후에는 이름으로 클라이언트를 해결할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$client = Mcp::client('github');
```

이름 있는 클라이언트는 요청별로 한 번만 해결되며, 요청 라이프사이클의 종료 시에 자동으로 절단됩니다.

### 클라이언트 인증

Bearer 토큰으로 보호되어 있는 Web MCP 서버로 접속하려면 `withToken` 메서드를 사용합니다. 토큰 문자열 또는 지연 해결하는 클로저를 전달할 수 있습니다.

```php theme={null}
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Client;

$client = Client::web('https://mcp.example.com')->withToken($token);

$client = Client::web('https://mcp.example.com')->withToken(
    fn () => Auth::user()->mcpToken(),
);
```

[OAuth 2.1](#oauth)로 보호되어 있는 서버에는 `withOAuth` 메서드를 사용합니다.

```php theme={null}
use Laravel\Mcp\Client;
use Laravel\Mcp\Facades\Mcp;

Mcp::registerClient('github', fn () => Client::web('https://mcp.example.com')->withOAuth(
    clientId: config('services.github_mcp.client_id'),
    clientSecret: config('services.github_mcp.client_secret'),
));
```

<Info>
  MCP 서버가 [동적 클라이언트 등록](https://datatracker.ietf.org/doc/html/rfc7591)을 지원하고 있는 경우, `clientId`와 `clientSecret`은 생략할 수 있습니다. 클라이언트가 자동 등록합니다.
</Info>

다음으로, `routes/ai.php` 파일에서 이름 있는 클라이언트의 OAuth 라우트를 `oAuthRoutesFor` 메서드를 사용해 등록합니다. 전달하는 클로저는 인가 코드와 액세스 토큰이 교환된 후에 클라이언트 이름과 `TokenSet`을 받습니다.

```php theme={null}
use Illuminate\Support\Facades\Auth;
use Laravel\Mcp\Client\OAuth\TokenSet;
use Laravel\Mcp\Facades\Mcp;

Mcp::oAuthRoutesFor('github', function (string $client, TokenSet $token) {
    Auth::user()->update([
        'github_mcp_token' => $token->accessToken,
    ]);

    return redirect('/dashboard');
});
```

이에 따라 2개의 이름이 있는 라우트가 등록됩니다. 사용자를 인가 서버로 리다이렉트하는 connect 라우트(`mcp.oauth.{client}.connect`)와, 인가 코드를 교환해 핸들러를 호출하는 callback 라우트(`mcp.oauth.{client}.callback`)입니다. 어느 쪽도 `web` 미들웨어 그룹을 사용합니다(`middleware` 인수로 덮어쓰기 가능).

인가 플로우를 개시하려면 사용자를 connect 라우트로 리다이렉트합니다.

```php theme={null}
return redirect()->route('mcp.oauth.github.connect');
```

### 도구

`tools` 메서드로 MCP 서버가 공개하는 도구를 취득합니다. 이름을 키로 한 컬렉션으로서 반환됩니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$tools = Mcp::client('github')->tools();

foreach ($tools as $tool) {
    $tool->name;
    $tool->title;
    $tool->description;
    $tool->inputSchema;
}
```

클라이언트는 자동으로 페이지네이션을 처리해 모든 도구를 취득합니다. `limit` 인수로 취득 수를 제한할 수 있습니다.

```php theme={null}
$tools = Mcp::client('github')->tools(limit: 10);
```

도구를 호출하려면 `callTool` 메서드를 사용하고, 도구 이름과 인수의 배열을 전달합니다. 반환되는 `ToolResult` 인스턴스로 응답을 취득합니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->callTool('current-weather', [
    'location' => 'New York',
]);

$result->text();             // 응답의 텍스트 콘텐츠
(string) $result;            // text() 와 동등
$result->isError;            // 도구가 에러를 보고했는지
$result->structuredContent;  // 구조화 콘텐츠(존재하는 경우)
```

일람 취득한 도구 인스턴스에서 직접 호출할 수도 있습니다.

```php theme={null}
$tools = Mcp::client('github')->tools();

$result = $tools['current-weather']->call([
    'location' => 'New York',
]);
```

[Laravel AI SDK](/ko/ai-sdk)로 에이전트를 구축하고 있는 경우, MCP 클라이언트의 도구를 에이전트에 직접 전달함으로써 모델이 프롬프트에의 응답 중에 그것들을 호출할 수 있게 됩니다. 자세한 내용은 AI SDK의 [MCP 도구](/ko/ai-sdk#mcp-tools) 섹션을 참조해 주십시오.

### 프롬프트

`prompts` 메서드로 MCP 서버가 공개하는 프롬프트를 취득합니다. 이름을 키로 한 컬렉션으로서 반환됩니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$prompts = Mcp::client('github')->prompts();

foreach ($prompts as $prompt) {
    $prompt->name;
    $prompt->title;
    $prompt->description;
    $prompt->arguments;
}
```

클라이언트는 자동으로 페이지네이션을 처리해 모든 프롬프트를 취득합니다. `limit` 인수로 취득 수를 제한할 수 있습니다.

```php theme={null}
$prompts = Mcp::client('github')->prompts(limit: 10);
```

프롬프트를 취득하려면 `getPrompt` 메서드를 사용하고, 프롬프트 이름과 인수의 배열을 전달합니다. 반환되는 `PromptResult` 인스턴스로 생성된 메시지를 취득할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->getPrompt('describe-weather', [
    'location' => 'New York',
]);

$result->text();        // 메시지의 텍스트 콘텐츠
(string) $result;       // text() 와 동등
$result->messages;      // 프롬프트가 반환한 메시지(원시 데이터)
$result->description;   // 프롬프트의 설명(존재하는 경우)
```

### 리소스

`resources` 메서드로 MCP 서버가 공개하는 리소스를 취득합니다. URI를 키로 한 컬렉션으로서 반환됩니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$resources = Mcp::client('github')->resources();

foreach ($resources as $resource) {
    $resource->uri;
    $resource->name;
    $resource->title;
    $resource->description;
    $resource->mimeType;
    $resource->size;
}
```

클라이언트는 자동으로 페이지네이션을 처리해 모든 리소스를 취득합니다. `limit` 인수로 취득 수를 제한할 수 있습니다.

```php theme={null}
$resources = Mcp::client('github')->resources(limit: 10);
```

리소스를 읽어 들이려면 `readResource` 메서드를 사용하고, 리소스의 URI를 전달합니다. 반환되는 `ResourceReadResult` 인스턴스로 리소스의 콘텐츠를 취득할 수 있습니다.

```php theme={null}
use Laravel\Mcp\Facades\Mcp;

$result = Mcp::client('github')->readResource('weather://guidelines');

$result->content();   // 리소스의 콘텐츠(base64 의 blob 은 자동으로 디코드)
(string) $result;     // content() 와 동등
$result->mimeType();  // 리소스의 MIME 타입(존재하는 경우)
$result->contents;    // 리소스가 반환한 콘텐츠(원시 데이터)
```

## 테스트

### MCP Inspector

MCP 서버의 동작 확인에는 인터랙티브한 디버그 도구 "MCP Inspector"를 사용합니다.

```shell theme={null}
# Web 서버
php artisan mcp:inspector mcp/weather

# 로컬 서버(이름이 "weather" 인 경우)
php artisan mcp:inspector weather
```

명령어를 실행하면 MCP Inspector가 기동해, 클라이언트 설정을 복사할 수 있습니다. 인증 미들웨어를 설정하고 있는 경우, Authorization 헤더를 포함해 접속해 주십시오.

### 유닛 테스트

도구·리소스·프롬프트에 대해 유닛 테스트를 쓸 수 있습니다.

<CodeGroup>
  ```php Pest theme={null}
  test('tool', function () {
      $response = WeatherServer::tool(CurrentWeatherTool::class, [
          'location' => 'Tokyo',
          'units' => 'celsius',
      ]);

      $response
          ->assertOk()
          ->assertSee('The current weather in Tokyo is 22°C and sunny.');
  });
  ```

  ```php PHPUnit theme={null}
  public function test_tool(): void
  {
      $response = WeatherServer::tool(CurrentWeatherTool::class, [
          'location' => 'Tokyo',
          'units' => 'celsius',
      ]);

      $response
          ->assertOk()
          ->assertSee('The current weather in Tokyo is 22°C and sunny.');
  }
  ```
</CodeGroup>

프롬프트와 리소스도 동일하게 테스트할 수 있습니다.

```php theme={null}
$response = WeatherServer::prompt(DescribeWeatherPrompt::class, ['tone' => 'casual']);
$response = WeatherServer::resource(WeatherGuidelinesResource::class);
```

인증된 사용자로서 실행하려면 `actingAs`를 사용합니다.

```php theme={null}
$response = WeatherServer::actingAs($user)->tool(CurrentWeatherTool::class, [...]);
```

주요 어서션 메서드는 다음과 같습니다.

```php theme={null}
$response->assertOk();           // 에러가 없음을 확인
$response->assertSee('...');     // 특정 텍스트가 포함됨을 확인
```

에러의 유무를 검증하려면 `assertHasErrors` / `assertHasNoErrors`를 사용합니다.

```php theme={null}
$response->assertHasErrors();

$response->assertHasErrors([
    'Something went wrong.',
]);

$response->assertHasNoErrors();
```

도구·리소스·프롬프트의 이름이나 타이틀, 설명을 검증할 수 있습니다.

```php theme={null}
$response->assertName('current-weather');
$response->assertTitle('Current Weather Tool');
$response->assertDescription('Fetches the current weather forecast for a specified location.');
```

스트리밍 응답의 노티피케이션을 검증하려면 `assertSentNotification`과 `assertNotificationCount`를 사용합니다.

```php theme={null}
$response->assertSentNotification('processing/progress', [
    'step' => 1,
    'total' => 5,
]);

$response->assertSentNotification('processing/progress', [
    'step' => 2,
    'total' => 5,
]);

$response->assertNotificationCount(5);
```

응답의 내용을 디버그하려면 `dd` 또는 `dump` 메서드를 사용합니다.

```php theme={null}
$response->dd();
$response->dump();
```


## Related topics

- [Laravel에서 MCP 서버 만들기](/ko/advanced/mcp-server.md)
- [Laravel AI SDK](/ko/ai-sdk.md)
- [Laravel Console Starter](/ko/packages/laravel-console-starter/index.md)
- [MCP](/ko/packages/laravel-copilot-sdk/mcp.md)
- [Laravel Boost](/ko/boost.md)
