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-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:)

磁盘上的 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 应用

AppResourceui:// 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 中读取这些。生成器工具使用 assertSentNotificationassertNotificationCount。最终结果仍然包含文本负载。

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 查询 UserTeam。它们通过命名的 directory 客户端查找人员,get_persondirectory://people/{id}。这就是为什么有两个服务器。如果你从门票工具中执行 User::find(),你就跳过了重点。

查询位于 DirectoryRepositoryTicketRepository 中。工具注入这些。

领域

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. 入门套件列加上 rolerequester\|agent\|admin)、team_id 可空、titleon_call

teams. nameslug。Support、Billing、Warehouse。

tickets. subjectbodystatusopen\|pending\|closed)、prioritylow\|normal\|high\|urgent)、requester_idassignee_id 可空、team_id 可空。

comments. ticket_iduser_idbody

articles. slugtitlebodyvisibilitypublic\|internal)。四行。公共的是退款和运输。内部的是升级和退款滥用。

LabSeederDatabaseSeeder 运行。它足够幂等,可以在 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 具有 versionauthor

add_comment

comment 策略检查后添加评论

assign_ticket

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

set_status

设置 openpendingclosed。这也用于关闭和重新打开

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_ticketget_ticket 拒绝接触 User。如果目录客户端被拔掉,分配会失败。Pest 证明了这一点。

提示词

提示词

功能

draft_reply

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

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:apithrottle:mcpmcp 限制器在 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 ticketsphp 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/McpTestCase 始终将 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$thisTestCall$this->travelTo 无法通过类型检查。

导入 Pest\Laravel\postJsonPest\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_ticketget_ticket 查询 UserTeam。不要将该查找移到 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} 读取使用相同的错误。

文档

-
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