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-сервер во что-нибудь полезное? Расскажите в комментариях, к чему вы его подключили.
Комментарии (0)
Пока нет комментариев — будьте первым.