はじめに
公式パッケージ laravel/mcp を使えば、数個のPHPクラスだけでLaravelアプリをMCPサーバーにできます。ClaudeやCursorなどのAIクライアントから、アプリのビジネスロジックやデータを直接呼び出せるようになります。
MCP(Model Context Protocol)は、Anthropicが提唱したオープンなプロトコルです。AIモデルが外部のツールやデータに接続する方法を標準化します。MCPサーバーは次の3種類の機能を提供します。
| 機能 | 役割 | 例 |
|---|---|---|
| Tool | AIが呼び出せるアクション | 注文の作成、天気の取得 |
| Resource | AIがコンテキストとして読む読み取り専用データ | ドキュメント、設定 |
| Prompt | 引数付きの再利用可能なプロンプトテンプレート | 要約、レビュー依頼 |
この記事では、シンプルなタスク管理用MCPサーバーを作ります。
- タスクを作成する Tool
- タスク一覧を返す Resource
- 作業内容を要約する Prompt
最後にMCP Inspectorで動作確認し、Claudeと接続します。
動作環境
- PHP 8.2 以上
- Laravel 13.x
- laravel/mcp(最新版)
パッケージ自体は Laravel 10 / PHP 8.1 以上に対応しています。
1. 環境の準備
新しいプロジェクトを作成し、パッケージをインストールします。
laravel new mcp-demo
cd mcp-demo
composer require laravel/mcp
AI用のルートファイルを公開します。
php artisan vendor:publish --tag=ai-routes
このコマンドで routes/ai.php が作成されます。通常のルートにおける routes/web.php と同じく、ここにMCPサーバーを登録します。
次に、サンプルデータ用の Task モデルを作成します。
php artisan make:model Task -m
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
$table->enum('priority', ['low', 'medium', 'high'])->default('medium');
$table->boolean('done')->default(false);
$table->timestamps();
});
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = ['title', 'description', 'priority', 'done'];
protected $casts = ['done' => 'boolean'];
}
php artisan migrate
2. MCPサーバーの作成
サーバーは中心となる存在で、AIクライアントが使えるTool・Resource・Promptをすべて宣言します。
php artisan make:mcp-server TaskServer
名前、バージョン、説明と、登録する機能(後のセクションで作るクラス)を記述します。
<?php
namespace App\Mcp\Servers;
use App\Mcp\Prompts\SummarizeTasksPrompt;
use App\Mcp\Resources\TaskListResource;
use App\Mcp\Tools\CreateTaskTool;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
#[Name('Task Server')]
#[Version('1.0.0')]
#[Instructions('タスク管理用サーバー:タスクの作成、一覧の取得、作業の要約ができます。')]
class TaskServer extends Server
{
protected array $tools = [
CreateTaskTool::class,
];
protected array $resources = [
TaskListResource::class,
];
protected array $prompts = [
SummarizeTasksPrompt::class,
];
}
サーバーの登録
routes/ai.php を開き、次のいずれかの方法で登録します。
<?php
use App\Mcp\Servers\TaskServer;
use Laravel\Mcp\Facades\Mcp;
// Webサーバー:HTTP POSTでアクセス。リモートのクライアント向け
Mcp::web('/mcp/tasks', TaskServer::class);
// ローカルサーバー:stdio経由のArtisanコマンドとして起動
Mcp::local('tasks', TaskServer::class);
| 種類 | 通信方式 | 向いているケース |
|---|---|---|
Mcp::web |
HTTP(Streamable HTTP、ストリーム時はSSE) | リモートクライアント、本番デプロイ、認証が必要な場合 |
Mcp::local |
php artisan mcp:start によるstdio |
開発マシン上でClaude Desktop、Claude Code、Cursorから使う場合 |
ローカルサーバーの場合、mcp:start を自分で実行する必要はありません。AIクライアントが接続時に自動で起動します。
3. Toolの作成
ToolはAIが呼び出せるアクションです。最初のToolとして、AIがデータベースに新しいタスクを作成できるようにします。
php artisan make:mcp-tool CreateTaskTool
<?php
namespace App\Mcp\Tools;
use App\Models\Task;
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 CreateTaskTool extends Tool
{
public function handle(Request $request): Response
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'priority' => ['in:low,medium,high'],
], [
'title.required' => 'タスクのタイトルを指定してください。例:「週次レポートを書く」',
'priority.in' => 'priority には low、medium、high のいずれかを指定してください。',
]);
$task = Task::create($validated);
return Response::structured([
'id' => $task->id,
'title' => $task->title,
'priority' => $task->priority,
'message' => "タスク #{$task->id} を作成しました。",
]);
}
public function schema(JsonSchema $schema): array
{
return [
'title' => $schema->string()
->description('タスクのタイトル')
->required(),
'description' => $schema->string()
->description('詳細な説明(任意)'),
'priority' => $schema->string()
->enum(['low', 'medium', 'high'])
->description('優先度')
->default('medium'),
];
}
}
ポイントは次のとおりです。
-
#[Description]は実質必須です。 説明は自動生成されません。AIはこの説明を見て、いつToolを呼ぶかを判断します。 -
Tool名はクラス名から自動で決まります。
CreateTaskToolの名前はcreate-taskになります。#[Name('...')]で変更できます。 schema()はAIへの引数の説明、validate()は実データの保護です。-
Response::structured()はJSON形式のデータを返します。 テキストだけならResponse::text()、業務エラーならResponse::error()を使います。
バリデーションエラーのメッセージはそのままAIに返されます。AIが自分で修正して再実行できるよう、具体的に書きましょう。
読み取り専用のToolには、安全であることをクライアントに伝えるアノテーションを付けましょう。
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
#[IsReadOnly]
class ListTasksTool extends Tool { /* ... */ }
Toolはサービスコンテナで解決されるため、リポジトリやサービスをコンストラクタや handle() メソッドにインジェクションできます。
4. ResourceとPromptの作成
Resource:タスク一覧
Resourceは、AIがコンテキストとして使う読み取り専用データです。各ResourceはURIとMIMEタイプを持ちます。
php artisan make:mcp-resource TaskListResource
<?php
namespace App\Mcp\Resources;
use App\Models\Task;
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;
#[Description('未完了のタスク一覧(優先度順)')]
#[Uri('tasks://open')]
#[MimeType('application/json')]
class TaskListResource extends Resource
{
public function handle(Request $request): Response
{
$tasks = Task::where('done', false)
->orderByRaw("FIELD(priority, 'high', 'medium', 'low')")
->get(['id', 'title', 'priority', 'created_at']);
return Response::text($tasks->toJson(JSON_UNESCAPED_UNICODE));
}
}
FIELD() はMySQLの関数です。SQLiteやPostgreSQLの場合は CASE WHEN 式に置き換えてください。
引数に応じた動的なResourceが必要な場合は、クラスに HasUriTemplate を実装し、new UriTemplate('tasks://{id}') を返します。変数 id は $request->get('id') で取得できます。
Prompt:作業の要約
Promptは、ユーザーがAIクライアント上で選べる会話テンプレートです。
php artisan make:mcp-prompt SummarizeTasksPrompt
<?php
namespace App\Mcp\Prompts;
use App\Models\Task;
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 SummarizeTasksPrompt extends Prompt
{
public function arguments(): array
{
return [
new Argument(
name: 'style',
description: '要約のスタイル:簡潔 または 詳細',
required: false,
),
];
}
public function handle(Request $request): array
{
$style = $request->string('style', '簡潔');
$tasks = Task::where('done', false)
->get(['title', 'priority'])
->toJson(JSON_UNESCAPED_UNICODE);
return [
Response::text("あなたはタスク管理アシスタントです。{$style}なスタイルで回答してください。")->asAssistant(),
Response::text("タスク一覧:{$tasks}。要約して、どれから取り組むべきか提案してください。"),
];
}
}
asAssistant() を付けたメッセージはAI側の発言として扱われ、それ以外はユーザーの入力として扱われます。
5. 動作確認とClaudeとの接続
MCP Inspectorで確認する
Laravel MCPにはMCP Inspectorが組み込まれています。ブラウザ上でToolの実行、Resourceの読み取り、Promptの確認ができます。
# Webサーバー(先に php artisan serve を起動)
php artisan mcp:inspector mcp/tasks
# ローカルサーバー
php artisan mcp:inspector tasks
Inspectorはクライアント設定も出力するので、ClaudeやCursorにそのままコピーできます。
ユニットテストを書く
テストからサーバー上のToolを直接呼び出せます(Pestの例)。
<?php
use App\Mcp\Servers\TaskServer;
use App\Mcp\Tools\CreateTaskTool;
test('タスクを作成できる', function () {
TaskServer::tool(CreateTaskTool::class, [
'title' => 'Qiitaの記事を書く',
'priority' => 'high',
])
->assertOk()
->assertSee('を作成しました');
});
test('titleがないとエラーになる', function () {
TaskServer::tool(CreateTaskTool::class, [])
->assertHasErrors();
});
Claude Codeと接続する
ローカルサーバー(stdio)の場合:
claude mcp add tasks -- php /absolute/path/to/mcp-demo/artisan mcp:start tasks
Webサーバー(HTTP)の場合:
claude mcp add --transport http tasks http://localhost:8000/mcp/tasks
Claude Desktopと接続する
claude_desktop_config.json に次を追加し、Claude Desktopを再起動します。
{
"mcpServers": {
"tasks": {
"command": "php",
"args": ["/absolute/path/to/mcp-demo/artisan", "mcp:start", "tasks"]
}
}
}
あとはチャットで次のように頼むだけです。
「PR #42 をレビュー」というタスクを優先度高で作って
Claudeが create-task Toolを自動で呼び出し、結果を返してくれます。
6. 認証とデプロイ時の注意点
インターネットに公開するWebサーバーには認証が必須です。Laravel MCPでは、通常のルートと同じようにミドルウェアを使えます。
方法1:Sanctum(シンプル)
Mcp::web('/mcp/tasks', TaskServer::class)
->middleware(['auth:sanctum', 'throttle:60,1']);
クライアントは Authorization: Bearer <token> ヘッダーを送信します。Claude Codeの場合:
claude mcp add --transport http tasks https://example.com/mcp/tasks \
--header "Authorization: Bearer YOUR_TOKEN"
方法2:PassportによるOAuth 2.1(推奨)
OAuth 2.1はMCP仕様で定められた認証方式で、最も多くのクライアントが対応しています。Passportをインストールしたうえで設定します。
Mcp::oauthRoutes();
Mcp::web('/mcp/tasks', TaskServer::class)
->middleware('auth:api');
php artisan vendor:publish --tag=mcp-views
use Laravel\Passport\Passport;
public function boot(): void
{
Passport::authorizationView(fn ($parameters) => view('mcp.authorize', $parameters));
}
ユーザーには、AIエージェントのアクセスを許可・拒否する画面が表示されます。
すでにSanctumを使っているアプリなら、OAuthにしか対応しないクライアントが必要になるまではSanctumのままで問題ありません。
Tool内での認可
Tool内では $request->user() で現在のユーザーを取得し、通常どおり権限をチェックします。
if (! $request->user()->can('create', Task::class)) {
return Response::error('タスクを作成する権限がありません。');
}
shouldRegister(Request $request): bool メソッドを使えば、権限のないユーザーからToolそのものを隠すこともできます。
デプロイ前のチェックリスト
-
Mcp::webに認証ミドルウェアとthrottleを付けた - 本当に必要なToolだけを公開している
-
データを書き込む・削除するToolは
handle()内で権限をチェックしている -
#[Description]とバリデーションメッセージを具体的に書いた - パスワード、トークン、個人情報などの機密データをレスポンスに含めていない
まとめ
PHPクラス4つと routes/ai.php の数行だけで、LaravelアプリがClaude、Cursor、GitHub Copilotから呼び出せる本格的なMCPサーバーになりました。Eloquent、バリデーション、サービスコンテナ、ミドルウェア、ポリシーなど、使い慣れたLaravelの機能をそのまま活かせるのが大きな魅力です。
次のステップとして、以下も試してみてください。
- Toolが多いサーバーでは、使用頻度の低いToolを 検索可能なToolカタログ(
ToolSearch)に移す - 時間のかかる処理には ストリーミングレスポンス(
Generatorを返す)を使う - MCP Apps で、ToolからインタラクティブなHTML画面をクライアント内に表示する
この記事が役に立ったら、ぜひ LGTM やコメントをお願いします!