1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LaravelでMCPサーバーを作る

1
Posted at

はじめに

公式パッケージ 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
database/migrations/xxxx_xx_xx_create_tasks_table.php
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();
    });
}
app/Models/Task.php
<?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

名前、バージョン、説明と、登録する機能(後のセクションで作るクラス)を記述します。

app/Mcp/Servers/TaskServer.php
<?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 を開き、次のいずれかの方法で登録します。

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
app/Mcp/Tools/CreateTaskTool.php
<?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には、安全であることをクライアントに伝えるアノテーションを付けましょう。

app/Mcp/Tools/ListTasksTool.php
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
app/Mcp/Resources/TaskListResource.php
<?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
app/Mcp/Prompts/SummarizeTasksPrompt.php
<?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の例)。

tests/Feature/CreateTaskToolTest.php
<?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を再起動します。

claude_desktop_config.json
{
  "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(シンプル)

routes/ai.php
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をインストールしたうえで設定します。

routes/ai.php
Mcp::oauthRoutes();

Mcp::web('/mcp/tasks', TaskServer::class)
    ->middleware('auth:api');
php artisan vendor:publish --tag=mcp-views
app/Providers/AppServiceProvider.php
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 やコメントをお願いします!

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?