Northwind Tickets
mcp-lab
一个 Laravel 应用,其存在目的是强制使用 Laravel MCP 的每个部分。假的帮助台。假的公司。用完后扔掉。
这是 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
相同的服务器类。两种进入方式。
Mcp::local('tickets', TicketsServer::class);
Mcp::web('/mcp/tickets', TicketsServer::class)->middleware(['auth:api', 'throttle:mcp']);本地是 stdio。主机将 php artisan mcp:start tickets 作为子进程运行。这是日常的 Cursor 循环。没有 HTTP 会话,没有 cookie,没有 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 |
|
多个文本块 |
|
资源链接 |
|
磁盘上的 Blob |
|
HTML 应用 |
|
进度 | 从生成器 |
Response::structured 不能为空,并返回工厂。要混合结构化 JSON 和资源链接,请构建两者并使用 withStructuredContent 附加负载。这就是 list_tickets 所做的。
类 $meta 是工具本身的元数据。->withMeta([...]) 是单个响应的元数据。create_ticket 有前者。get_ticket 有后者。
注解
主机的提示。它们不会在 PHP 中强制执行任何内容。策略仍然会。
属性 | 这里的含义 |
| 不写入 |
| 可以安全重试 |
| 删除或破坏状态 |
| 可能接触外部世界。 |
| 资源提示 |
| 此工具打开 MCP 应用 |
shouldRegister(Request $request): bool 从列表中隐藏工具、资源或提示。如果主机仍然尝试调用隐藏的,服务器返回未找到。用于角色门控。delete_ticket 仅限管理员。仍然在 handle() 内部检查 $request->user()->can(...)。列出和执行是不同的门。
授权
$request->user() 是已登录用户,与控制器相同。调用 $user->can('update', $ticket) 并返回 Response::error('Permission denied.')。不要发明第二个认证系统。
本地服务器没有 HTTP 会话。此应用的门票服务器在 boot() 中当 Auth 为空时以 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 会死锁,因为该进程是单线程的。对于同应用调用,请使用本地。
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 是 bearer token。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 变量作为第二个参数。助手展开 desk://tickets/{id}。
assertSee 只读取文本和结构化数据。资源链接和 _meta 位于原始 JSON-RPC 负载上。此仓库的 mcpRpc() 和 mcpToolContent() 在 tests/Helpers.php 中读取这些。生成器工具使用 assertSentNotification 和 assertNotificationCount。最终结果仍然包含文本负载。
Web 认证是 HTTP 测试。使用 mcpTicketsCall() 对 POST /mcp/tickets 进行测试。未认证的请求必须是 401,而不是登录重定向。
此仓库是什么
Northwind Support。一个 Laravel 13 应用,PHP 8.4,SQLite。Inertia 和 React 仅用于转储页面。代理是写入路径。
两个 MCP 服务器,每个注册两次,本地和 Web。
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}。这就是为什么有两个服务器。如果你从门票工具中执行 User::find(),你就跳过了重点。
查询位于 DirectoryRepository 和 TicketRepository 中。工具注入这些。
领域
users.role 上有三个角色。
角色 | 他们能做什么 |
| 打开门票,评论自己的,阅读公共知识库 |
| 查看所有门票,分配,评论,更改状态,阅读内部知识库 |
| 代理能做的一切,加上删除门票和每周回顾提示 |
策略是普通的 Laravel 策略。TicketPolicy 涵盖查看、更新、评论和删除。ArticlePolicy 对请求者隐藏内部文章。员工是 Role::isStaff(),代理或管理员。
登录用户来自 LabSeeder。所有用户的密码都是 password。
邮箱 | 角色 |
| requester |
| agent |
| admin |
email_verified_at 已设置。Fortify 启用了验证。User 不实现 MustVerifyEmail,所以你不会被阻止。
Jonah Hale,jonah@northwind.test,是值班代理。
表
保持它们小。如果工具不需要某列,它就不存在。
users. 入门套件列加上 role(requester\|agent\|admin)、team_id 可空、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 可空、team_id 可空。
comments. ticket_id、user_id、body。
articles. slug、title、body、visibility(public\|internal)。四行。公共的是退款和运输。内部的是升级和退款滥用。
LabSeeder 从 DatabaseSeeder 运行。它足够幂等,可以在 migrate:fresh 后重新运行。二十张门票,三十条评论,八个人,一个 PNG 在 storage/app/badge.png。
DirectoryServer
只读。说明如此。名称 Northwind Directory,版本 0.0.1,青色图标。
工具
工具 | 功能 |
| 按姓名、电子邮件或职务搜索。可选团队 slug。结构化 |
| 查找一个用户 ID。自定义验证消息。结构化 |
| 无必需参数。两个文本块,先是名称,再是 slug |
| 返回 |
资源
URI | 功能 |
| 静态 Markdown。 |
| 人员档案。 |
| 团队档案。第二个模板,因此第一个不是一次性使用 |
| 谁在值班。 |
| 通过 |
这里没有提示词。它们属于工单服务器,在那里它们有话要说。
TicketsServer
名称 Northwind Tickets,版本 0.0.1,深色图标。
当没有用户认证时,boot() 以 sam@northwind.test 身份登录。本地 Cursor 没有 HTTP 会话。没有这个,每个工具都会说 You must be signed in.。Web 请求已经有 Passport 用户,因此 boot() 会提前返回。
工具
工具 | 功能 |
| 列出用户可以看到的工单。状态和优先级过滤器。结构化输出以及 |
| 单个工单。分配人和请求人姓名来自 |
| 打开一个工单。类 |
| 在 |
|
|
| 设置 |
|
|
| 关闭所有超过 N 天的 |
| 搜索当前用户可以看到的文章。结构化 |
| 模型可见。 |
| 同一个应用, |
list_tickets 不能使用 TicketResource::uri() 来生成链接。该方法返回模板 desk://tickets/{id}。链接必须是展开后的字符串。
assign_ticket 和 get_ticket 拒绝接触 User。如果目录客户端被拔掉,分配会失败。Pest 证明了这一点。
提示词
提示词 | 功能 |
| 接受 |
| 接受 |
| 未结工单的回顾。 |
资源
URI | 功能 |
| 静态 Markdown,高优先级。如何分类 |
| 当前用户可以看到的未结工单的 Markdown 列表 |
| 带评论的 Markdown 档案 |
| 一篇文章。缺失和禁止的 slug 返回相同的错误 |
| 仅限员工查看的内部升级文章列表 |
| 交互式队列 iframe |
desk://queue 是 Markdown。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。一个访问令牌可用于两个服务器。
批准和拒绝屏幕是 resources/views/mcp/authorize.blade.php。保留它。
仪表板
Fortify 和 Inertia 来自入门套件。不要替换它们。
/dashboard 是两个表格。工单显示 ID、主题、状态、优先级、请求人和分配人。人员显示 ID、姓名、角色、团队和值班状态。Wayfinder 命名路由。DashboardController 传递 Inertia props。没有创建工单的表单。
通过刷新 /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 服务器。
Inspector。 php artisan mcp:inspector tickets 和 php artisan mcp:inspector mcp/tickets。当工具无操作且需要原始结果时使用。Web Inspector 需要 Passport 承载令牌。Inspector UI 在 :6274 上,此应用在 :8000 上。mcp/*、oauth/* 和 .well-known/* 的 CORS 位于 config/cors.php 中。没有这些路径,发现 GET 返回 200,但浏览器仍然会丢弃它们。
仪表板。 证明写工具触及 Eloquent。
Pest。 php artisan test --compact tests/Feature/Mcp。
不要从工单工具中调用 Client::web('http://127.0.0.1:8000/mcp/directory'),而 artisan serve 是唯一的 PHP 进程。它会挂起。composer run dev 不会改变这一点。在 :8001 上运行第二个 PHP 服务器并配合 Client::web 是可选的后续实验,不是默认设置。
测试此仓库
功能测试位于 tests/Feature/Mcp。TestCase 始终将 directory 客户端重新绑定到 InProcessDirectoryTransport,以便 SQLite :memory: 可见。DirectoryClientTest 中的一个测试使用真实的 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() 并期望策略错误。调用它会得到未找到的 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 调用它们。
套件涵盖的内容:
请求人不能分配或删除
代理可以分配,不能删除
管理员可以删除
以 Sam 身份时
delete_ticket不存在,以 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是 Markdown。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