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

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。

形状

方式

文本

Response::text('...')

错误

Response::error('Permission denied.')

结构化 JSON

Response::structured($payload) 加上 outputSchema()

多个文本块

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

资源链接

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

磁盘上的 Blob

Response::fromStorage('badge.png')

HTML 应用

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

进度

从生成器 yield Response::notification('processing/progress', [...])

Response::structured 不能为空,并返回工厂。要混合结构化 JSON 和资源链接,请构建两者并使用 withStructuredContent 附加负载。这就是 list_tickets 所做的。

类 $meta 是工具本身的元数据。->withMeta([...]) 是单个响应的元数据。create_ticket 有前者。get_ticket 有后者。

注解

主机的提示。它们不会在 PHP 中强制执行任何内容。策略仍然会。

属性

这里的含义

#[IsReadOnly]

不写入

#[IsIdempotent]

可以安全重试

#[IsDestructive]

删除或破坏状态

#[IsOpenWorld]

可能接触外部世界。who_is_on_call 即使轮班是假的也佩戴此属性

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

资源提示

#[RendersApp]

此工具打开 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 上有三个角色。

角色

他们能做什么

requester

打开门票,评论自己的,阅读公共知识库

agent

查看所有门票,分配,评论,更改状态,阅读内部知识库

admin

代理能做的一切,加上删除门票和每周回顾提示

策略是普通的 Laravel 策略。TicketPolicy 涵盖查看、更新、评论和删除。ArticlePolicy 对请求者隐藏内部文章。员工是 Role::isStaff(),代理或管理员。

登录用户来自 LabSeeder。所有用户的密码都是 password。

邮箱

角色

ada@northwind.test

requester

sam@northwind.test

agent

root@northwind.test

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,青色图标。

工具

工具

功能

search_people

按姓名、电子邮件或职务搜索。可选团队 slug。结构化 { people } 并带有 outputSchema。#[IsReadOnly] 和 #[IsIdempotent]

get_person

查找一个用户 ID。自定义验证消息。结构化 { person }

list_teams

无必需参数。两个文本块,先是名称,再是 slug

who_is_on_call

返回 on_call 用户。在假轮值表上使用 #[IsOpenWorld],以便使用该注解

资源

URI

功能

directory://org

静态 Markdown。#[MimeType]、#[Priority(0.9)]、#[Audience(Role::Assistant)]

directory://people/{id}

人员档案。HasUriTemplate、$request->get('id')

directory://teams/{slug}

团队档案。第二个模板,因此第一个不是一次性使用

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() 会提前返回。

工具

工具

功能

list_tickets

列出用户可以看到的工单。状态和优先级过滤器。结构化输出以及 desk://tickets/{id} 资源链接

get_ticket

单个工单。分配人和请求人姓名来自 directory://people/{id}。withMeta(['source' => 'eloquent'])

create_ticket

打开一个工单。类 $meta 具有 version 和 author

add_comment

在 comment 策略检查后添加评论

assign_ticket

#[IsIdempotent]。使用 Mcp::client('directory')->callTool('get_person', ...) 解析人员

set_status

设置 open、pending 或 closed。这也用于关闭和重新打开

delete_ticket

#[IsDestructive]。shouldRegister 仅对管理员为 true

close_stale_tickets

关闭所有超过 N 天的 open 工单。处理过程中产生 processing/progress 事件

search_kb

搜索当前用户可以看到的文章。结构化 { articles } 并带有 desk://kb/{slug}

show_queue

模型可见。#[RendersApp(resource: QueueApp::class)]

get_queue_data

同一个应用,visibility: [Visibility::App]。iframe 刷新而不向模型提供第二个列表工具

list_tickets 不能使用 TicketResource::uri() 来生成链接。该方法返回模板 desk://tickets/{id}。链接必须是展开后的字符串。

assign_ticket 和 get_ticket 拒绝接触 User。如果目录客户端被拔掉,分配会失败。Pest 证明了这一点。

提示词

提示词

功能

draft_reply

接受 ticket_id 和 tone。加载工单。助手消息以及包含真实主题的用户消息

triage_ticket

接受 ticket_id。使用有用的错误字符串进行验证

weekly_review

未结工单的回顾。shouldRegister 对请求人为 false

资源

URI

功能

desk://playbook

静态 Markdown,高优先级。如何分类

desk://queue

当前用户可以看到的未结工单的 Markdown 列表

desk://tickets/{id}

带评论的 Markdown 档案

desk://kb/{slug}

一篇文章。缺失和禁止的 slug 返回相同的错误

desk://kb/escalation

仅限员工查看的内部升级文章列表

ui:// QueueApp

交互式队列 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/call HTTP 请求体

要断言工具被隐藏,使用 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} 读取使用相同的错误。

文档

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    B
    maintenance
    An MCP server that exposes the Tickiti helpdesk API to AI assistants, enabling ticket management and helpdesk operations via natural language.
    11
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    An 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
  • F
    license
    Not graded
    quality
    B
    maintenance
    MCP 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.
    -