Claude уже умеет работать с вашим приложением на Laravel. И для этого не нужно вручную создавать REST API, писать клиент и описывать для него каждый эндпоинт — достаточно предоставить несколько инструментов через протокол Model Context Protocol (MCP) и позволить модели вызывать их напрямую. Официальный пакет  laravel/mcp  позволяет сделать это буквально за полдня.

Это практическое руководство. Мы создадим небольшой MCP-сервер для интернет-магазина: инструменты, которые сможет вызывать модель, ресурс для чтения контекста, валидацию входных данных, авторизацию и тест, чтобы все работало честно. Полный разбор того, как устроен этот протокол, опубликован на нашем сайте: Use Laravel to create your own MCP server. Здесь же мы займемся написанием кода.

Сначала кратко о том, что именно мы строим. MCP-сервер предоставляет подключенному ИИ-клиенту три вещи:  инструменты (действия, которые модель может вызывать, например поиск заказов),  ресурсы (данные только для чтения, подгружаемые для контекста) и  промпты (переиспользуемые шаблоны). Клиент и сервер общаются по протоколу JSON-RPC, а  laravel/mcp  берет на себя работу с этим форматом связи, так что вам нужно писать только на PHP.

Системные требования

  • Свежее приложение на Laravel (отлично подойдет версия 12.x).
  • PHP 8.2 или новее.
  • Composer.
  • MCP-клиент для последующего подключения. Подойдут как Claude Desktop, так и Claude Code, а для тестирования предусмотрен встроенный MCP Inspector.

Шаг 1: Установка пакета

Загрузите его с помощью Composer:

 composer require laravel/mcp
 

Затем опубликуйте файл маршрутов, в котором будут регистрироваться ваши серверы:

 php artisan vendor:publish --tag=ai-routes
 

Это создаст файл  routes/ai.php . Обращайтесь с ним так же, как с  routes/web.php : для каждого создаваемого сервера там прописывается отдельная строка.

Шаг 2: Создание сервера

Сервер — это то, к чему подключается клиент. Он объединяет ваши инструменты, ресурсы и промпты под одним именем. Сгенерируйте сервер:

 php artisan make:mcp-server OrdersServer
 

Вы получите файл  app/Mcp/Servers/OrdersServer.php . Этот класс определяет свою идентичность через атрибуты и перечисляет доступные сущности в трех массивах:

 <?php

namespace App\Mcp\Servers;

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

#[Name('Orders Server')]
#[Version('1.0.0')]
#[Instructions('Search orders, add internal notes, and cancel orders for the shop.')]
class OrdersServer extends Server
{
    protected array $tools = [
        SearchOrdersTool::class,
    ];

    protected array $resources = [
        //
    ];

    protected array $prompts = [
        //
    ];
}
 

Не пропускайте атрибут  Instructions . Он отправляется клиенту в качестве контекста о назначении сервера, чтобы модель понимала, с чем имеет дело, прежде чем что-либо вызывать.

Шаг 3: Регистрация сервера

Сервер ничего не делает, пока не зарегистрирован в  routes/ai.php . Они бывают двух видов. Веб-сервер доступен по HTTP для удаленных клиентов, а локальный сервер запускается как команда Artisan для агентов на том же компьютере, например Claude Code.

 use App\Mcp\Servers\OrdersServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/orders', OrdersServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

Mcp::local('orders', OrdersServer::class);
 

Веб-серверы представляют собой обычные маршруты, поэтому к ним применяются привычные вам middleware. К строке авторизации мы вернемся на шаге 8.

Шаг 4: Создание инструмента (tool)

Это компонент, который выполняет реальную работу. Сгенерируйте инструмент:

 php artisan make:mcp-tool SearchOrdersTool
 

У инструмента есть два метода.  schema  объявляет принимаемые аргументы, а  handle  выполняет работу и возвращает ответ. Вот пример поиска заказов только для чтения:

 <?php

namespace App\Mcp\Tools;

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

#[IsReadOnly]
#[Description('Search recent orders, optionally filtered by status, and return their references and totals.')]
class SearchOrdersTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'status' => ['nullable', 'in:pending,shipped,delivered,cancelled'],
            'limit' => ['integer', 'between:1,50'],
        ]);

        $orders = Order::query()
            ->when($validated['status'] ?? null, fn ($query, $status) => $query->where('status', $status))
            ->latest()
            ->limit($validated['limit'] ?? 10)
            ->get();

        if ($orders->isEmpty()) {
            return Response::text('No orders matched that search.');
        }

        return Response::structured([
            'count' => $orders->count(),
            'orders' => $orders->map(fn ($order) => [
                'reference' => $order->reference,
                'status' => $order->status,
                'total' => $order->total,
                'placed_at' => $order->created_at->toIso8601String(),
            ])->all(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'status' => $schema->string()
                ->enum(['pending', 'shipped', 'delivered', 'cancelled'])
                ->description('Only return orders with this status.'),

            'limit' => $schema->integer()
                ->description('Maximum number of orders to return.')
                ->default(10),
        ];
    }
}
 

Он уже добавлен в массив  $tools  сервера на шаге 2, так что клиент может вызывать его прямо сейчас.

Здесь стоит отметить две вещи. Схема представляет собой типизированный контракт для модели:  status  принимает одно из четырех значений, а  limit  является целым числом по умолчанию. А благодаря атрибуту  Description  модель понимает, когда нужно задействовать этот инструмент, поэтому пишите описание так, будто инструктируете человека, который никогда не видел ваш код. Метод  Response::structured()  возвращает структурированные данные, при этом сохраняя версию в виде простого текста для клиентов, которым это необходимо.

Шаг 5: Валидация входных данных и написание понятных для модели ошибок

Схема задает структуру. Валидатор Laravel обеспечивает соблюдение правил точно так же, как в контроллере:

 $validated = $request->validate([
    'reference' => ['required', 'string', 'max:32'],
], [
    'reference.required' => 'Provide the order reference, for example "ORD-10423".',
]);
 

А вот и тот нюанс, который часто упускают. Сообщение об ошибке возвращается модели, и модель решает, что делать дальше, основываясь на его тексте. Сообщение вроде «The reference field is required» ни о чем ей не говорит. Фраза «Provide the order reference, for example ORD-10423» объясняет, как именно следует повторить запрос. Пишите сообщения валидации как инструкции для читателя, который будет по ним действовать.

Шаг 6: Описание поведения инструмента для клиента

Аннотации описывают поведение инструмента, не меняя его логики. Клиент использует их, чтобы решить, как представить инструмент пользователю — например, запросить подтверждение перед выполнением любых действий, вносящих изменения. Это обычные атрибуты:

 <?php

namespace App\Mcp\Tools;

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

#[IsDestructive]
#[IsIdempotent]
#[Description('Cancel an order. An order that is already cancelled is left unchanged.')]
class CancelOrderTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'reference' => ['required', 'string', 'max:32'],
        ]);

        $order = Order::where('reference', $validated['reference'])->firstOrFail();

        if ($order->status !== 'cancelled') {
            $order->update(['status' => 'cancelled']);
        }

        return Response::text("Order {$order->reference} is cancelled.");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'reference' => $schema->string()
                ->description('The reference of the order to cancel.')
                ->required(),
        ];
    }
}
 

Всего их четыре:  #[IsReadOnly]  (ничего не меняет),  #[IsDestructive]  (может производить деструктивные изменения),  #[IsIdempotent]  (повторный запуск с теми же аргументами не приводит к дальнейшим изменениям) и  #[IsOpenWorld]  (взаимодействует с системами за пределами вашего приложения). Отмена заказа — это деструктивное, но идеопотентное действие: отмените его дважды, и оно останется просто отмененным. Добавьте  CancelOrderTool::class  в массив  $tools  сервера, чтобы его можно было вызывать.

Шаг 7: Добавление ресурса и промпта

Инструменты — это действия. Остальные два примитива дополняют общую картину.

Ресурс представляет собой контекст только для чтения, который модель может подгружать. Никаких аргументов, только метод  handle , возвращающий содержимое, например политику возврата:

 <?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('The shop returns and refunds policy.')]
class RefundPolicyResource extends Resource
{
    public function handle(Request $request): Response
    {
        return Response::text(file_get_contents(resource_path('policies/refunds.md')));
    }
}
 

Промпт — это переиспользуемый шаблон, который клиент может предложить пользователю. Он объявляет свои аргументы и возвращает сообщения:

 <?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('Draft a short status update to send to a customer about their order.')]
class OrderUpdatePrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'reference',
                description: 'The order the update is about.',
                required: true,
            ),
        ];
    }

    public function handle(Request $request): array
    {
        $reference = $request->string('reference');

        return [
            Response::text('You write friendly customer service messages.')->asAssistant(),
            Response::text("Draft a short update for the customer about order {$reference}."),
        ];
    }
}
 

Сгенерируйте их с помощью команд  make:mcp-resource  и  make:mcp-prompt , а затем зарегистрируйте в массивах  $resources  и  $prompts  сервера.

Шаг 8: Обеспечение безопасности сервера

Веб-сервер MCP — это публичный эндпоинт, который может читать и изменять ваши данные. Оставить его открытым — такая же ошибка, как выпустить админ-API без авторизации. Поскольку это обычный маршрут, вы защищаете его с помощью middleware.

Простой вариант — использование токена через Laravel Sanctum. Клиент передает его в заголовке  Authorization :

 Mcp::web('/mcp/orders', OrdersServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);
 

Для сторонних клиентов более надежным выбором будет OAuth 2.1 через Laravel Passport. Зарегистрируйте маршруты обнаружения и примените гард Passport:

 Mcp::oauthRoutes();

Mcp::web('/mcp/orders', OrdersServer::class)
    ->middleware('auth:api');
 

Как только пользователь аутентифицирован, данные запроса передаются в ваши инструменты, поэтому  $request->user()  работает как обычно. Вы даже можете скрывать инструмент для определенных пользователей с помощью метода  shouldRegister :

 public function shouldRegister(Request $request): bool
{
    return $request->user()?->can('manage-orders') ?? false;
}
 

Инструмент, чей метод  shouldRegister  возвращает false, никогда не появится в списке клиента и не сможет быть вызван. Один сервер, но разные инструменты для разных пользователей.

Шаг 9: Инспекция и тестирование

Есть два способа проверить сервер до того, как к нему обратится реальный клиент.

MCP Inspector подключается к серверу и выводит список его инструментов, ресурсов и промптов, чтобы вы могли вызвать их вручную. Укажите зарегистрированный сервер по имени:

 php artisan mcp:inspector orders
 

Он выведет настройки клиента для копирования в ваш MCP-клиент. Если сервер защищен авторизацией, заголовок нужно будет добавить и там.

Для автоматизированного тестирования напишите обычный тест и вызовите примитив на сервере, который его регистрирует. Ответ содержит вспомогательные методы для утверждений (assertions):

 <?php

use App\Mcp\Servers\OrdersServer;
use App\Mcp\Tools\CancelOrderTool;
use App\Models\Order;

it('cancels an order', function () {
    $order = Order::factory()->create([
        'reference' => 'ORD-10423',
        'status' => 'shipped',
    ]);

    $response = OrdersServer::tool(CancelOrderTool::class, [
        'reference' => 'ORD-10423',
    ]);

    $response
        ->assertOk()
        ->assertSee('ORD-10423 is cancelled');

    expect($order->fresh()->status)->toBe('cancelled');
});
 

Существуют соответствующие проверки для ошибок и уведомлений, а также хелпер  actingAs  для тестирования инструментов, зависящих от аутентифицированного пользователя.

Заключение

Вы прошли путь от пустого приложения на Laravel до MCP-сервера с инструментом чтения, инструментом записи, ресурсом, промптом, аутентификацией и тестом. Теперь модель работает с вашим приложением через единый стандартный интерфейс, и при этом вы не написали ни единой строки кода протокола. Дальнейший паттерн прост: добавьте инструмент, дайте ему четкое описание и понятные аннотации, валидируйте входящие данные с помощью читаемых сообщений и защитите его подходящим middleware.

Более подробное объяснение того, как устроен MCP, читайте в полной статье на нашем сайте: Use Laravel to create your own MCP server.

А вы уже интегрировали MCP-сервер во что-нибудь полезное? Расскажите в комментариях, к чему вы его подключили.