Skip to main content
Glama
jaksa-v
by jaksa-v

mcp-lab

Laravel MCP のすべての部分を強制的に動かすための Laravel アプリです。偽のヘルプデスク。偽の会社。使い終わったら捨ててください。

これは Laravel で MCP がどのように動作するか、そしてこのリポジトリがそれをどう使うかのデフォルトのリファレンスです。

これは製品ではなくジムです。タイプフェイスを選んだり、請求ページを発明したりしている自分に気づいたら、やめてください。

Laravel MCP の仕組み

Model Context Protocol は JSON-RPC です。AI ホストがサーバーが公開しているものをリストアップし、それを呼び出します。Cursor と Inspector が、このリポジトリが対象とするホストです。Laravel MCP、ここでは laravel/mcp 0.9.x がラッパーです。

プロトコルを発明するのではありません。PHP クラスを書いて登録するのです。

サーバー

サーバーは Laravel\Mcp\Server を継承したクラスです。ツール、リソース、プロンプトのカタログです。

#[Name('Northwind Tickets')]
#[Version('0.0.1')]
#[Instructions('...')]
class TicketsServer extends Server
{
    protected array $tools = [/* ... */];
    protected array $resources = [/* ... */];
    protected array $prompts = [/* ... */];
}

#[Name]#[Version]#[Instructions]#[Icon] は、ホストがモデルに表示するメタデータです。Instructions はそのサーバーのシステムプロンプトです。短く、実用的に保ってください。

php artisan make:mcp-server で作成します。routes/ai.php に登録します。Laravel はそのファイルを自動的に読み込みます。bootstrap/app.php に追加しないでください。

ローカルと Web

同じサーバークラス。2 つの入り口があります。

Mcp::local('tickets', TicketsServer::class);
Mcp::web('/mcp/tickets', TicketsServer::class)->middleware(['auth:api', 'throttle:mcp']);

ローカルは stdio です。ホストは子プロセスとして php artisan mcp:start tickets を実行します。これが日常の Cursor ループです。HTTP セッションも、クッキーも、Passport もありません。

Web はそのパスでの HTTP JSON-RPC です。Inspector とリモートホストがこれを使います。ミドルウェアが適用され、ここに OAuth が存在します。

routes/ai.phpweb ミドルウェアグループでラップしないでください。CSRF が Inspector をブロックします。

ツール、リソース、プロンプト

サーバーはツール、リソース、プロンプトを公開できます。make:mcp-toolmake:mcp-resourcemake:mcp-prompt で生成します。次に、クラスをサーバーの配列に追加します。未登録のクラスは何もしません。

ツールはアクションです。モデルは引数付きでそれらを呼び出します。schema() はホストが宣伝する JSON Schema です。handle(Request $request) が作業を行います。$request->validate() は通常の Laravel バリデーションです。モデルが行動できるメッセージを書いてください。たとえば Give me the ticket id, like 12. のように。The ticket_id field is required. ではなく。

リソースは URI にある読み取り可能なドキュメントです。静的 URI は #[Uri('desk://playbook')] を使用します。テンプレートは HasUriTemplate を実装し、$request->get('id') で変数を読み取ります。MIME タイプは #[MimeType] を使用します。ホストはツールを呼び出さずにそれらをリストアップして読み取ることができます。

プロンプトは再利用可能なメッセージテンプレートです。arguments() はホストが収集すべきものを宣言します。handle() はメッセージを返します。通常はアシスタントの指示と、実際のデータを含むユーザーメッセージです。モデルはそれから書き込みます。

コンストラクタでリポジトリを注入してください。ツールクラスにクエリを置かないでください。handle() は Laravel のサービスを型ヒントにすることもできます。

レスポンス

handle()Laravel\Mcp\Response、レスポンスファクトリ、レスポンスの配列、または Generator を返します。

形状

方法

テキスト

Response::text('...')

エラー

Response::error('Permission denied.')

構造化 JSON

Response::structured($payload) に加えて outputSchema()

複数のテキストブロック

Response::make([Response::text(...), Response::text(...)])

リソースリンク

Response::resourceLink(uri:, name:, mimeType:, title:)

ディスクからのブロブ

Response::fromStorage('badge.png')

HTML アプリ

Response::view('mcp.queue-app', [...])

進捗

ジェネレータから yield Response::notification('processing/progress', [...])

Response::structured は空にできず、ファクトリを返します。構造化 JSON とリソースリンクを混在させるには、両方を構築し、withStructuredContent でペイロードを添付します。それが list_tickets が行うことです。

クラス $meta はツール自体のメタデータです。->withMeta([...]) は 1 つのレスポンスのメタデータです。create_ticket には前者があります。get_ticket には後者があります。

アノテーション

ホストへのヒントです。PHP では何も強制しません。ポリシーは依然として強制します。

属性

ここでの意味

#[IsReadOnly]

書き込みを行わない

#[IsIdempotent]

再試行しても安全

#[IsDestructive]

削除または状態を壊す

#[IsOpenWorld]

外部の世界に触れる可能性がある。who_is_on_call はローテが偽であってもこれを付けている

#[Priority]#[Audience]#[LastModified]

リソースのヒント

#[RendersApp]

このツールは MCP アプリを開く

shouldRegister(Request $request): bool は、ツール、リソース、またはプロンプトをリストから隠します。ホストが隠されたものを呼び出そうとした場合、サーバーは not-found を返します。ロールゲートに使用します。delete_ticket は管理者のみです。それでも handle() 内で $request->user()->can(...) をチェックしてください。リストアップと実行は別のドアです。

認可

$request->user() はサインインしたユーザーで、コントローラと同じです。$user->can('update', $ticket) を呼び出し、Response::error('Permission denied.') を返します。2 番目の認証システムを発明しないでください。

ローカルサーバーには HTTP セッションがありません。このアプリのチケットサーバーは、Auth が空の場合、boot() で Sam としてサインインします。Web サーバーは Passport からユーザーを取得します。

MCP クライアント

Laravel は MCP サーバーを呼び出すこともできます。名前付きクライアントはサービスプロバイダに存在します。

Mcp::registerClient('directory', fn () => Client::local('php', [
    'artisan', 'mcp:start', 'directory',
]));

次に、チケットコードは Mcp::client('directory')->callTool('get_person', ['id' => $id]) または ->readResource('directory://people/'.$id) を実行します。

Client::local はプロセスを生成します。Client::web($url) は HTTP です。php artisan serve に対する同一アプリの HTTP は、そのプロセスがシングルスレッドであるためデッドロックします。同一アプリの呼び出しには local を使用してください。

MCP アプリ

AppResourceui:// URI で自己完結型の HTML ドキュメントを返します。#[RendersApp(resource: QueueApp::class)] とマークされたツールは、対応するホストにその HTML をフェッチしてサンドボックス化された iframe に入れるように指示します。

Blade ビューは <x-mcp::app> を使用します。このコンポーネントはクライアント SDK を同梱します。iframe 内では、createMcpAppapp.callServerTool(...) を提供します。Vite と React は適用されません。Tailwind と Alpine は #[AppMeta(libraries: [Library::Tailwind, Library::Alpine])] から来ます。

Visibility::App はツールをモデルから隠し、iframe だけがそれを呼び出せるようにします。get_queue_data がそのツールです。

Cursor はこれらのツールをリストアップします。iframe はレンダリングしません。Pest がクラスが機能することを確認する方法です。

Web での認証

Laravel ドキュメントは Sanctum と Passport を提供しています。Sanctum はベアラートークンです。Passport は OAuth 2.1 で、プロトコルが指定するものです。

このアプリは Passport を使用します。Mcp::oauthRoutes() はディスカバリと動的クライアント登録を登録します。Web ルートは auth:api を使用します。Laravel MCP は単一の mcp:use スコープを宣伝します。mcp-views を公開し、Passport::authorizationViewresources/views/mcp/authorize.blade.php に向けます。その Blade はそのままにしてください。

テスト

Inspector は突くためのものです。Pest はポリシーが保持されることを確認する方法です。

TicketsServer::actingAs($sam)
    ->tool(ListTicketsTool::class, ['status' => 'open'])
    ->assertOk()
    ->assertSee('...');

TicketsServer::resource(TicketResource::class, ['id' => $ticket->id]);
TicketsServer::prompt(DraftReplyPrompt::class, ['ticket_id' => $ticket->id, 'tone' => 'curt']);

テンプレートリソースは URI 変数を 2 番目の引数として取ります。ヘルパーは desk://tickets/{id} を展開します。

assertSee はテキストと構造化データのみを読み取ります。リソースリンクと _meta は生の JSON-RPC ペイロードに存在します。このリポジトリの tests/Helpers.php にある mcpRpc()mcpToolContent() がそれを読み取ります。ジェネレータツールは assertSentNotificationassertNotificationCount を使用します。最終結果は依然としてテキストペイロードを保持します。

Web 認証は HTTP テストです。mcpTicketsCall()POST /mcp/tickets を実行します。未認証のリクエストは 401 でなければならず、ログインリダイレクトではありません。

このリポジトリが何であるか

Northwind Support。1 つの Laravel 13 アプリ、PHP 8.4、SQLite。Inertia と React はダンプページのみです。エージェントが書き込みパスです。

2 つの MCP サーバー。それぞれローカルと Web の 2 回登録されています。

routes/ai.php

Mcp::local('directory', DirectoryServer::class);
Mcp::local('tickets', TicketsServer::class);

Mcp::oauthRoutes();

Mcp::web('/mcp/directory', DirectoryServer::class)
    ->middleware(['auth:api', 'throttle:mcp']);

Mcp::web('/mcp/tickets', TicketsServer::class)
    ->middleware(['auth:api', 'throttle:mcp']);

DirectoryServer は読み取り専用の人物とチームです。TicketsServer はヘルプデスクです。チケットツールは Eloquent を通じて UserTeam をクエリしてはいけません。名前付き directory クライアント、get_persondirectory://people/{id} を通じて人物を調べます。それが 2 つのサーバーがある理由です。チケットツールから User::find() を実行した場合、要点をスキップしたことになります。

クエリは DirectoryRepositoryTicketRepository に存在します。ツールはこれらを注入します。

ドメイン

users.role には 3 つのロールがあります。

ロール

できること

requester

チケットを開く、自分のチケットにコメントする、公開 KB を読む

agent

すべてのチケットを見る、割り当てる、コメントする、ステータスを変更する、内部 KB を読む

admin

エージェントができるすべてに加えて、チケットの削除と週次レビュープロンプト

ポリシーは通常の Laravel ポリシーです。TicketPolicy は view、update、comment、delete をカバーします。ArticlePolicy は内部記事を requester から隠します。スタッフは Role::isStaff()、つまり agent または admin です。

ログインユーザーは LabSeeder から来ます。全員のパスワードは password です。

メール

ロール

ada@northwind.test

requester

sam@northwind.test

agent

root@northwind.test

admin

email_verified_at が設定されています。Fortify は検証を有効にしています。UserMustVerifyEmail を実装していないため、ブロックされることはありません。

Jonah Hale、jonah@northwind.test がオンコールエージェントです。

テーブル

小さく保ってください。ツールに必要な列でなければ、そこにありません。

users. スターターキットの列に加えて role (requester\|agent\|admin)、team_id は nullable、titleon_call

teams. nameslug。Support、Billing、Warehouse。

tickets. subjectbodystatus (open\|pending\|closed)、priority (low\|normal\|high\|urgent)、requester_idassignee_id は nullable、team_id は nullable。

comments. ticket_iduser_idbody

articles. slugtitlebodyvisibility (public\|internal)。4 行。公開されているのは refunds と shipping。内部は escalation と refund-abuse。

LabSeederDatabaseSeeder から実行されます。migrate:fresh の後に再実行しても十分に冪等です。20 のチケット、30 のコメント、8 人の人物、storage/app/badge.png にある 1 つの PNG。

DirectoryServer

読み取り専用。指示がそう言っています。名前 Northwind Directory、バージョン 0.0.1、ティールのアイコン。

ツール

Tool

説明

search_people

名前、メール、または役職で検索。オプションのチームスラッグ。outputSchema を持つ構造化 { people }#[IsReadOnly]#[IsIdempotent]

get_person

1つのユーザーIDを検索。カスタム検証メッセージ。構造化 { person }

list_teams

必須引数なし。2つのテキストブロック、名前とスラッグ

who_is_on_call

on_call ユーザーを返す。フェイクロタに #[IsOpenWorld] を付けてアノテーションが使われるようにする

リソース

URI

説明

directory://org

静的マークダウン。#[MimeType]#[Priority(0.9)]#[Audience(Role::Assistant)]

directory://people/{id}

人物のドシエ。HasUriTemplate$request->get('id')

directory://teams/{slug}

チームのドシエ。2番目のテンプレートなので最初のものは一回きりではない

directory://on-call

オンコール担当者。#[LastModified]

directory://badge

Response::fromStorage('badge.png') による小さなPNG

ここにはプロンプトはありません。プロンプトはチケットサーバーにあり、そこで言うべきことがあります。

TicketsServer

名前 Northwind Tickets、バージョン 0.0.1、ダークアイコン。

boot() は、認証されたユーザーがいない場合に sam@northwind.test としてサインインします。ローカルの Cursor には HTTP セッションがありません。これがないと、すべてのツールが You must be signed in. と言うでしょう。Web リクエストには既に Passport ユーザーがいるため、boot() は早期に戻ります。

ツール

Tool

説明

list_tickets

ユーザーが表示できるチケットを一覧表示。ステータスと優先度のフィルター。構造化出力に加えて desk://tickets/{id} リソースリンク

get_ticket

1つのチケット。担当者と依頼者の名前は directory://people/{id} から取得。withMeta(['source' => 'eloquent'])

create_ticket

チケットを開く。クラス $meta には versionauthor がある

add_comment

comment ポリシーチェックの後にコメントを追加

assign_ticket

#[IsIdempotent]Mcp::client('directory')->callTool('get_person', ...) で人物を解決

set_status

openpending、または closed を設定。これはクローズと再オープンも兼ねる

delete_ticket

#[IsDestructive]shouldRegister は管理者のみ true

close_stale_tickets

N 日より古いすべての open チケットをクローズ。進行中に processing/progress を生成

search_kb

現在のユーザーが表示できる記事を検索。desk://kb/{slug} を持つ構造化 { articles }

show_queue

モデル可視。#[RendersApp(resource: QueueApp::class)]

get_queue_data

同じアプリ、visibility: [Visibility::App]。iframe はモデルに2番目のリストツールを渡さずに更新

list_tickets はリンクに TicketResource::uri() を使用できません。そのメソッドはテンプレート desk://tickets/{id} を返します。リンクは展開された文字列でなければなりません。

assign_ticketget_ticketUser に触れることを拒否します。ディレクトリクライアントが切断されている場合、割り当ては失敗します。Pest がそれを証明します。

プロンプト

Prompt

説明

draft_reply

ticket_idtone を受け取る。チケットを読み込む。アシスタントメッセージと、実際の件名を含むユーザーメッセージ

triage_ticket

ticket_id を受け取る。有用なエラー文字列で検証

weekly_review

オープンチケットの要約。shouldRegister はリクエスターに対して false

リソース

URI

説明

desk://playbook

静的マークダウン、高優先度。トリアージの方法

desk://queue

現在のユーザーが表示できるオープンチケットのマークダウンリスト

desk://tickets/{id}

コメント付きマークダウンのドシエ

desk://kb/{slug}

1つの記事。存在しないスラッグと禁止されたスラッグは同じエラーを返す

desk://kb/escalation

内部エスカレーション記事のスタッフ専用リスト

ui:// QueueApp

インタラクティブなキューの iframe

desk://queue はマークダウンです。QueueApp は iframe です。混同しないでください。

QueueApp

QueueAppAppResource を拡張します。Blade は resources/views/mcp/queue-app.blade.php にあります。Alpine は <x-mcp::app> 内にあります。更新は app.callServerTool を通じて get_queue_data を呼び出します。

これは Inertia ページではありません。React で書き直さないでください。

ホストが MCP Apps をレンダリングできない場合でも、クラスと Pest テストが証明になります。

認証とWebサーバー

/mcp/tickets/mcp/directory の両方が auth:apithrottle:mcp を使用します。mcp リミッターは AppServiceProvider で毎分60回で、ユーザーIDまたはIPでキー付けされます。

UserOAuthenticatable を実装し、Passport の HasApiTokens を使用します。インターフェースがないのは、通常の Passport セットアップのバグです。

認証されていない POST は 401 を返し、WWW-Authenticate がそのパスの保護されたリソースメタデータを指します。ディスカバリーは /mcp/tickets/mcp/directory の両方をカバーします。動的登録は POST /oauth/register です。1つのアクセストークンが両方のサーバーで機能します。

承認および拒否画面は resources/views/mcp/authorize.blade.php です。そのままにしてください。

ダッシュボード

Fortify と Inertia はスターターキットから来ています。置き換えないでください。

/dashboard は2つのテーブルです。チケットは id、件名、ステータス、優先度、依頼者、担当者を表示します。人物は id、名前、役割、チーム、オンコールを表示します。Wayfinder がルートを名付けます。DashboardController は Inertia プロパティを渡します。チケットを作成するフォームはありません。

書き込みツールが機能したことを証明するには、/dashboard を更新します。React ページを気にする場合は composer run dev を実行します。

Passport の承認と QueueApp の iframe は Blade のままです。パッケージがそれらを所有しています。

それとの対話方法

Cursor、ローカル。 .cursor/mcp.json はプロジェクトルートから両方のサーバーを起動します。

{
    "mcpServers": {
        "northwind-tickets": {
            "command": "php",
            "args": ["artisan", "mcp:start", "tickets"]
        },
        "northwind-directory": {
            "command": "php",
            "args": ["artisan", "mcp:start", "directory"]
        }
    }
}

チケットは日常のループです。クライアント作業のために Cursor でディレクトリは必要ありません。チケットサーバープロセスがそれを生成します。

ローカルチケットは Sam として実行されます。Ada または Root を表示するには、Pest の actingAs またはトークン付きのWebサーバーを使用します。

インスペクター。 php artisan mcp:inspector ticketsphp artisan mcp:inspector mcp/tickets。ツールが何もせず、生の結果が必要な場合にこれを使用します。Web Inspector には Passport ベアラートークンが必要です。Inspector UI は :6274 にあり、このアプリは :8000 にあります。CORS は config/cors.phpmcp/*oauth/*.well-known/* にあります。これらのパスがないと、ディスカバリー GET は 200 を返しますが、ブラウザはそれらを破棄します。

ダッシュボード。 書き込みツールが Eloquent にヒットすることを証明します。

Pest。 php artisan test --compact tests/Feature/Mcp

artisan serve が唯一の PHP プロセスである場合、チケットツールから Client::web('http://127.0.0.1:8000/mcp/directory') を呼び出さないでください。ハングします。composer run dev はそれを変更しません。:8001 の2番目の PHP サーバーと Client::web はオプションの後日の実験であり、デフォルトではありません。

このリポジトリのテスト

機能テストは tests/Feature/Mcp にあります。TestCase は常に directory クライアントを InProcessDirectoryTransport に再バインドして、SQLite :memory: が見えるようにします。DirectoryClientTest の1つのテストは、実際の stdio クライアントを使用してツールを一覧表示します。切断されたクライアントテストは、名前付きクライアントを php -r 'exit(1);' に向けます。

tests/Helpers.php のヘルパー:

  • mcpRpc($response) は生の JSON-RPC 配列用

  • mcpToolContent($response) は結果コンテンツリスト用

  • mcpTicketsCall($name, $arguments)tools/call HTTP ボディ用

ツールが非表示であることをアサートするには、TicketsServer::actingAs($user) の後に (new TicketsServer(new FakeTransporter))->createContext()->tools() を実行します。非表示のツールで handle() を呼び出してポリシーエラーを期待しないでください。それを呼び出すと、not-found JSON-RPC エラーになります。

アノテーションは $tool->annotations() から読み取ります。toArray()['annotations'] からではありません。Tool::toArray() はそのフィールドを array|object として型付けします。リソースの toArray() はアノテーションを省略します。

時間を固定するには Carbon::setTestNow を使用します。pest-plugin-phpstan がない場合、$thisTestCall であり、$this->travelTo は型チェックされません。

Pest\Laravel\postJsonPest\Laravel\withToken をインポートします。$this で呼び出さないでください。

スイートがカバーする内容:

  • リクエスターは割り当てまたは削除ができない

  • エージェントは割り当て可能、削除は不可

  • 管理者は削除可能

  • delete_ticket は Sam として実行時には存在せず、Root には存在する

  • 内部エスカレーションリソースは Ada から隠されている

  • search_kb はリクエスターから内部記事を隠す

  • weekly_review はスタッフ専用

  • assign_ticket はディレクトリクライアントが人物を見つけられない場合に失敗する

  • 検証エラーは文である

  • close_stale_tickets は進行状況通知を生成する

  • OAuth ディスカバリー、登録、承認 Blade、PKCE、そして両方のWebサーバーでのツール呼び出し

スターターキットの Fortify テストはそのまま残ります。MCP を証明するために書き直さないでください。

ファイルレイアウト

app/
  Enums/Role.php Status.php Priority.php Visibility.php
  Models/User.php Team.php Ticket.php Comment.php Article.php
  Policies/TicketPolicy.php ArticlePolicy.php
  Repositories/DirectoryRepository.php TicketRepository.php ArticleRepository.php
  Http/Controllers/DashboardController.php
  Mcp/
    Servers/DirectoryServer.php TicketsServer.php
    Tools/          (15 tools)
    Resources/      (11 resources, including QueueApp)
    Prompts/        (3 prompts)
routes/ai.php
resources/js/pages/dashboard.tsx
resources/views/mcp/authorize.blade.php
resources/views/mcp/queue-app.blade.php
database/seeders/LabSeeder.php
tests/Feature/Mcp/
tests/Helpers.php
tests/Support/InProcessDirectoryTransport.php
.ai/rules/          (settled decisions for the next agent)

常設の罠

これらは既に .ai/rules にあります。簡単に再び壊れるため、ここにも属します。

  • チケットツールは Client::local を通じて DirectoryServer と通信します。テストはそれをインプロセストランスポートでオーバーライドします。

  • assign_ticket または get_ticket から UserTeam をクエリしないでください。そのルックアップを TicketRepository に移動しないでください。

  • PHPDoc のディレクトリ $resourcesclass-string<Server\Resource> として型付けします。Pint は class-string<Resource> を PHP の resource 型に小文字化します。

  • ai.php は Web ミドルウェアグループの外に留まります。

  • QueueApp は ui:// です。desk://queue はマークダウンです。

  • KB 記事は ArticleRepository を通ります。存在しない desk://kb/{slug} 読み取りと禁止された読み取りは同じエラーを使用します。

ドキュメント

-
license - not tested
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.

  • Hosted MCP endpoint with realistic fake data for prototyping agents. 12 tools, no setup.

  • MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jaksa-v/mcp-lab'

If you have feedback or need assistance with the MCP directory API, please join our Discord server