Northwind Tickets
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.php を web ミドルウェアグループでラップしないでください。CSRF が Inspector をブロックします。
ツール、リソース、プロンプト
サーバーはツール、リソース、プロンプトを公開できます。make:mcp-tool、make:mcp-resource、make: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 を返します。
形状 | 方法 |
テキスト |
|
エラー |
|
構造化 JSON |
|
複数のテキストブロック |
|
リソースリンク |
|
ディスクからのブロブ |
|
HTML アプリ |
|
進捗 | ジェネレータから |
Response::structured は空にできず、ファクトリを返します。構造化 JSON とリソースリンクを混在させるには、両方を構築し、withStructuredContent でペイロードを添付します。それが list_tickets が行うことです。
クラス $meta はツール自体のメタデータです。->withMeta([...]) は 1 つのレスポンスのメタデータです。create_ticket には前者があります。get_ticket には後者があります。
アノテーション
ホストへのヒントです。PHP では何も強制しません。ポリシーは依然として強制します。
属性 | ここでの意味 |
| 書き込みを行わない |
| 再試行しても安全 |
| 削除または状態を壊す |
| 外部の世界に触れる可能性がある。 |
| リソースのヒント |
| このツールは 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 アプリ
AppResource は ui:// URI で自己完結型の HTML ドキュメントを返します。#[RendersApp(resource: QueueApp::class)] とマークされたツールは、対応するホストにその HTML をフェッチしてサンドボックス化された iframe に入れるように指示します。
Blade ビューは <x-mcp::app> を使用します。このコンポーネントはクライアント SDK を同梱します。iframe 内では、createMcpApp が app.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::authorizationView を resources/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() がそれを読み取ります。ジェネレータツールは assertSentNotification と assertNotificationCount を使用します。最終結果は依然としてテキストペイロードを保持します。
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 を通じて User や Team をクエリしてはいけません。名前付き directory クライアント、get_person と directory://people/{id} を通じて人物を調べます。それが 2 つのサーバーがある理由です。チケットツールから User::find() を実行した場合、要点をスキップしたことになります。
クエリは DirectoryRepository と TicketRepository に存在します。ツールはこれらを注入します。
ドメイン
users.role には 3 つのロールがあります。
ロール | できること |
| チケットを開く、自分のチケットにコメントする、公開 KB を読む |
| すべてのチケットを見る、割り当てる、コメントする、ステータスを変更する、内部 KB を読む |
| エージェントができるすべてに加えて、チケットの削除と週次レビュープロンプト |
ポリシーは通常の Laravel ポリシーです。TicketPolicy は view、update、comment、delete をカバーします。ArticlePolicy は内部記事を requester から隠します。スタッフは Role::isStaff()、つまり agent または admin です。
ログインユーザーは LabSeeder から来ます。全員のパスワードは password です。
メール | ロール |
| requester |
| agent |
| admin |
email_verified_at が設定されています。Fortify は検証を有効にしています。User は MustVerifyEmail を実装していないため、ブロックされることはありません。
Jonah Hale、jonah@northwind.test がオンコールエージェントです。
テーブル
小さく保ってください。ツールに必要な列でなければ、そこにありません。
users. スターターキットの列に加えて role (requester\|agent\|admin)、team_id は nullable、title、on_call。
teams. name、slug。Support、Billing、Warehouse。
tickets. subject、body、status (open\|pending\|closed)、priority (low\|normal\|high\|urgent)、requester_id、assignee_id は nullable、team_id は nullable。
comments. ticket_id、user_id、body。
articles. slug、title、body、visibility (public\|internal)。4 行。公開されているのは refunds と shipping。内部は escalation と refund-abuse。
LabSeeder は DatabaseSeeder から実行されます。migrate:fresh の後に再実行しても十分に冪等です。20 のチケット、30 のコメント、8 人の人物、storage/app/badge.png にある 1 つの PNG。
DirectoryServer
読み取り専用。指示がそう言っています。名前 Northwind Directory、バージョン 0.0.1、ティールのアイコン。
ツール
Tool | 説明 |
| 名前、メール、または役職で検索。オプションのチームスラッグ。 |
| 1つのユーザーIDを検索。カスタム検証メッセージ。構造化 |
| 必須引数なし。2つのテキストブロック、名前とスラッグ |
|
|
リソース
URI | 説明 |
| 静的マークダウン。 |
| 人物のドシエ。 |
| チームのドシエ。2番目のテンプレートなので最初のものは一回きりではない |
| オンコール担当者。 |
|
|
ここにはプロンプトはありません。プロンプトはチケットサーバーにあり、そこで言うべきことがあります。
TicketsServer
名前 Northwind Tickets、バージョン 0.0.1、ダークアイコン。
boot() は、認証されたユーザーがいない場合に sam@northwind.test としてサインインします。ローカルの Cursor には HTTP セッションがありません。これがないと、すべてのツールが You must be signed in. と言うでしょう。Web リクエストには既に Passport ユーザーがいるため、boot() は早期に戻ります。
ツール
Tool | 説明 |
| ユーザーが表示できるチケットを一覧表示。ステータスと優先度のフィルター。構造化出力に加えて |
| 1つのチケット。担当者と依頼者の名前は |
| チケットを開く。クラス |
|
|
|
|
|
|
|
|
| N 日より古いすべての |
| 現在のユーザーが表示できる記事を検索。 |
| モデル可視。 |
| 同じアプリ、 |
list_tickets はリンクに TicketResource::uri() を使用できません。そのメソッドはテンプレート desk://tickets/{id} を返します。リンクは展開された文字列でなければなりません。
assign_ticket と get_ticket は User に触れることを拒否します。ディレクトリクライアントが切断されている場合、割り当ては失敗します。Pest がそれを証明します。
プロンプト
Prompt | 説明 |
|
|
|
|
| オープンチケットの要約。 |
リソース
URI | 説明 |
| 静的マークダウン、高優先度。トリアージの方法 |
| 現在のユーザーが表示できるオープンチケットのマークダウンリスト |
| コメント付きマークダウンのドシエ |
| 1つの記事。存在しないスラッグと禁止されたスラッグは同じエラーを返す |
| 内部エスカレーション記事のスタッフ専用リスト |
| インタラクティブなキューの iframe |
desk://queue はマークダウンです。QueueApp は iframe です。混同しないでください。
QueueApp
QueueApp は AppResource を拡張します。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:api と throttle:mcp を使用します。mcp リミッターは AppServiceProvider で毎分60回で、ユーザーIDまたはIPでキー付けされます。
User は OAuthenticatable を実装し、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 tickets と php artisan mcp:inspector mcp/tickets。ツールが何もせず、生の結果が必要な場合にこれを使用します。Web Inspector には Passport ベアラートークンが必要です。Inspector UI は :6274 にあり、このアプリは :8000 にあります。CORS は config/cors.php の mcp/*、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/callHTTP ボディ用
ツールが非表示であることをアサートするには、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 がない場合、$this は TestCall であり、$this->travelTo は型チェックされません。
Pest\Laravel\postJson と Pest\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からUserやTeamをクエリしないでください。そのルックアップをTicketRepositoryに移動しないでください。PHPDoc のディレクトリ
$resourcesはclass-string<Server\Resource>として型付けします。Pint はclass-string<Resource>を PHP のresource型に小文字化します。ai.phpは Web ミドルウェアグループの外に留まります。QueueApp は
ui://です。desk://queueはマークダウンです。KB 記事は
ArticleRepositoryを通ります。存在しないdesk://kb/{slug}読み取りと禁止された読み取りは同じエラーを使用します。
ドキュメント
This server cannot be installed
Maintenance
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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