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,而不是登录重定向。
Related MCP server: tickiti-mcp
此仓库是什么
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 deployed
Maintenance
Related MCP Connectors
- ZapierOAuthcom.zapier
Hosted MCP server connecting AI assistants to 9,000+ apps and 40,000+ actions via Zapier.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
AlicenseAqualityDmaintenanceAn MCP server that lets AI assistants search and read support tickets from Jitbit Helpdesk. Works with both SaaS and on-premise installations.333 npm3MIT
tickiti-mcpofficial
AlicenseBqualityBmaintenanceAn MCP server that exposes the Tickiti helpdesk API to AI assistants, enabling ticket management and helpdesk operations via natural language.11MIT- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables AI assistants to interact with Freshservice service desk, supporting ticket search, retrieval, and creation via a configurable, Docker-first deployment.MIT
- FlicenseNot gradedqualityBmaintenanceMCP server that provides IT support tools to search known incidents, check ticket status, and create ticket drafts, enabling an AI assistant to help users with IT support queries.-