🤖 Laravel Vibes
用于实现机器控制协议 (MCP) 服务器的 Laravel 扩展包
projectsaturnstudios
README
🤖 Laravel Vibes
一个强大的 Laravel 扩展包,用于实现机器控制协议 (MCP) 服务器,从而在你的应用程序中实现与 AI 代理的无缝集成。通过结构化的工具定义和实时通信,构建智能的、交互式的功能。
<details> <summary>📖 目录</summary>
- 🤖 Laravel Vibes
</details>
✨ 主要特性
- 🔧 工具注册系统 - 定义、注册和暴露工具供 AI 代理使用
- 🔄 服务器发送事件 (SSE) - 通过事件流与 AI 代理进行实时通信
- 🛣️ API 端点 - 用于 MCP 协议实现的即用型端点
- 🧩 原始处理器 - 用于各种 MCP 原始类型的可扩展系统
- 🔍 自动发现 - 自动发现代码库中的工具实现
- 🦺 类型安全 - 完整的 PHP 8.2+ 类型提示和返回类型声明
- 🔮 面向未来 - 为即将到来的 MCP 功能(资源、提示、示例和根)奠定基础,并提供框架
🚦 实现状态
本节清晰地概述了当前已实现的 MCP 原始类型以及计划在未来版本中发布的原始类型。
原始实现状态
原始类型 | 状态 | 引入版本 | 备注 |
---|---|---|---|
工具 | ✅ 完整 | 0.1.0 | 完整的实现,包括存储库、数据对象和发现 |
资源 | 🚧 部分 | 0.4.0 | 框架已就位,API 尚未最终确定 |
提示 | 🚧 部分 | 0.4.0 | 框架已就位,API 尚未最终确定 |
示例 | 🚧 部分 | 0.4.0 | 框架已就位,API 尚未最终确定 |
根 | 🚧 部分 | 0.4.0 | 框架已就位,API 尚未最终确定 |
特性可用性矩阵
特性 | v0.1.0 | v0.2.0 | v0.3.0 | v0.4.0 | 计划中 |
---|---|---|---|---|---|
工具注册 | ✅ | ✅ | ✅ | ✅ | ✅ |
服务器发送事件 | ✅ | ✅ | ✅ | ✅ | ✅ |
自动发现 | ❌ | ✅ | ✅ | ✅ | ✅ |
原始类型缓存 | ❌ | ❌ | ✅ | ✅ | ✅ |
资源原始类型 | ❌ | ❌ | ❌ | 🚧 | ✅ |
提示原始类型 | ❌ | ❌ | ❌ | 🚧 | ✅ |
示例原始类型 | ❌ | ❌ | ❌ | 🚧 | ✅ |
根原始类型 | ❌ | ❌ | ❌ | 🚧 | ✅ |
图例:
- ✅ 完整实现
- 🚧 部分实现
- ❌ 未实现
📚 文档
Laravel Vibes 的完整文档位于 docs/
目录中:
文档文件 | 描述 |
---|---|
TheAgency.md | TheAgency 类的详细文档,它是 MCP 原始类型的中心协调器 |
ClassDiagram.md | 显示类之间关系的结构图 |
UsageExamples.md | 在应用程序中使用 Laravel Vibes 的实际示例 |
CONTRIBUTING.md | 贡献 Laravel Vibes 的指南 |
Flow-Diagrams.md | Laravel Vibes 架构和工作流程的可视化图表 |
Examples.md | Laravel Vibes 实现的真实世界示例 |
index.md | 概述和核心概念文档 |
类图
下面是一个简化的类图,显示了 Laravel Vibes 的主要组件:
classDiagram
class TheAgency {
+addPrimitiveHandler(mixed primitiveHandlerClass)
+removePrimitiveHandler(string primitiveHandlerClass)
+addTool(mixed tool)
+getTool(string name)
}
class VibeTool {
#string name
+static getMetadata() array
+getName() string
}
class PrimitiveHandler {
<<interface>>
+getName() string
}
TheAgency o-- VibeTool : manages
VibeTool ..|> PrimitiveHandler : implements
🔧 安装
你可以通过 composer 安装该扩展包:
composer require projectsaturnstudios/laravel-vibes
该扩展包将自动向 Laravel 注册其服务提供者。
⚙️ 配置
使用以下命令发布配置文件:
php artisan vendor:publish --tag="vibes"
这会将配置文件发布到 config/vibes.php
和 config/cors.vibes.php
。
主要配置选项:
return [
'service_info' => [
'server_name' => env('VIBE_SVC_NAME', 'laravel-vibes-server'),
'server_version' => env('VIBE_SVC_VERSION', '1.0.0'),
'heartbeat_interval' => 20, // 秒
'catch_exceptions' => false, // 在 MCP 事件循环中捕获异常
],
'features' => [
'tools' => true, // 工具注册和执行(完全实现)
'resources' => false, // 资源发现和访问(部分实现)
'prompts' => false, // 提示模板和执行(部分实现)
'samples' => false, // 模型行为配置(部分实现)
'roots' => false, // 自定义工作流入口点(部分实现)
],
'auto_discover_all_primitives' => [app()->path()],
'auto_discover_base_path' => base_path(),
// 工具存储库和路由配置...
];
👉 了解更多: 有关高级配置选项,请参阅 TheAgency.md 中的配置部分。
🚀 使用
注册工具
在你的服务提供者中注册 AI 工具:
use App\Tools\TextAnalysisTool;
use App\Tools\DataFetchTool;
// 在服务提供者的 boot 方法中
public function boot()
{
$agency = app('the-agency');
$agency->addTools([
TextAnalysisTool::class,
DataFetchTool::class,
]);
}
或者通过配置文件注册:
'tools' => [
'text-analysis' => \App\Tools\TextAnalysisTool::class,
'data-fetch' => \App\Tools\DataFetchTool::class,
],
定义自定义工具
创建一个 AI 代理可以使用的工具:
<?php
namespace App\Tools;
use ProjectSaturnStudios\Vibes\Primitives\Tools\Data\VibeTool;
class WeatherTool extends VibeTool
{
protected string $name = 'weather';
public static function getMetadata(): array
{
return [
'name' => 'weather',
'description' => '获取某个位置的当前天气',
'parameters' => [
'type' => 'object',
'properties' => [
'location' => [
'type' => 'string',
'description' => '城市名称或位置'
]
],
'required' => ['location']
]
];
}
public function handle(string $location)
{
// 实现天气查找逻辑
return [
'location' => $location,
'temperature' => 72,
'conditions' => 'sunny'
];
}
}
使用服务器发送事件
使用 SSE 将 AI 代理连接到你的应用程序:
// 前端 JavaScript
const eventSource = new EventSource('/mcp/sse');
const sessionId = generateUniqueId();
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received update:', data);
};
处理 AI 代理消息
处理来自代理的传入 MCP 消息:
// POST 到 /mcp/sse/messages
{
"jsonrpc": "2.0",
"method": "run_tool",
"session_id": "session-12345",
"id": "req-1",
"params": {
"name": "weather",
"input": {
"location": "San Francisco"
}
}
}
👉 了解更多: 详细的使用示例可在 UsageExamples.md 文档中找到。
🔍 工具发现 & 注册
Laravel Vibes 遵循与 Laravel 事件溯源类似的方法来加载原始类型,使用自动发现和注册模式。 有关更详细的示例,请参阅 UsageExamples.md 文档。
自动发现
该扩展包自动发现并注册配置目录中的工具和其他 MCP 原始类型:
// config/vibes.php
'auto_discover_all_primitives' => [app()->path()], // 要扫描原始类型的目录
'auto_discover_base_path' => base_path(), // 相对目录的基本路径
在应用程序引导期间,Laravel Vibes 扫描这些目录中实现 PrimitiveHandler
接口的类,并自动将它们注册到 TheAgency:
protected function discoverPrimitiveHandlers() : void
{
$agency = app(TheAgency::class);
$cachedPrimitiveHandlers = $this->getCachedPrimitiveHandlers();
if (! is_null($cachedPrimitiveHandlers)) {
$agency->addPrimitiveHandlers($cachedPrimitiveHandlers);
return;
}
(new PrimitiveHandlerDiscoveryService)
->within(config('vibes.auto_discover_all_primitives'))
->useBasePath(config('vibes.auto_discover_base_path', base_path()))
->ignoringFiles(Composer::getAutoloadedFiles(base_path('composer.json')))
->addToTheAgency($agency);
}
手动注册
对于手动注册,请修改你的 AppServiceProvider
或创建一个专用的服务提供者:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use ProjectSaturnStudios\Vibes\TheAgency;
use App\MCP\Tools\WeatherTool;
use App\MCP\Tools\TranslationTool;
use App\MCP\Resources\UserResource;
use App\MCP\Prompts\GreetingPrompt;
class MCPServiceProvider extends ServiceProvider
{
public function boot()
{
// 获取 agency 单例
$agency = app(TheAgency::class);
// 注册工具
$agency->addTools([
WeatherTool::class,
TranslationTool::class,
]);
// 注册其他原始类型(如果已实现)
// $agency->addResources([
// UserResource::class,
// ]);
// $agency->addPrompts([
// GreetingPrompt::class,
// ]);
}
}
然后将你的提供者添加到 config/app.php
:
'providers' => [
// 其他服务提供者...
App\Providers\MCPServiceProvider::class,
],
缓存原始类型
对于生产环境,Laravel Vibes 提供了缓存机制来提高性能。 发现的原始类型缓存在 bootstrap/cache/vibes.php
中。
要在开发期间清除缓存:
php artisan cache:clear
你还可以实现一个专用命令来管理原始类型缓存(类似于 Laravel 事件溯源的 event-sourcing:cache-event-handlers
):
// 在你的服务提供者中注册该命令
$this->app->booted(function () {
$this->app['events']->listen('cache:clearing', function () {
// 清除缓存的原始类型
@unlink($this->app['config']['vibes.service_info.cache_path'].'/vibes.php');
});
});
🧪 综合测试指南
本节提供有关测试 Laravel Vibes 组件的指导和示例,重点是自定义工具和 AI 代理交互。
单元测试自定义工具
测试你的自定义工具,以确保它们正确处理输入并产生预期的输出:
<?php
namespace Tests\Unit\Tools;
use Tests\TestCase;
use App\Tools\WeatherTool;
use ProjectSaturnStudios\Vibes\TheAgency;
use ProjectSaturnStudios\Vibes\Exceptions\InvalidToolParameters;
class WeatherToolTest extends TestCase
{
protected TheAgency $agency;
protected WeatherTool $weatherTool;
protected function setUp(): void
{
parent::setUp();
$this->agency = app(TheAgency::class);
$this->weatherTool = new WeatherTool();
// 向 TheAgency 注册该工具
$this->agency->addTool($this->weatherTool);
}
/** @test */
public function it_has_correct_metadata()
{
$metadata = WeatherTool::getMetadata();
$this->assertEquals('weather', $metadata['name']);
$this->assertArrayHasKey('description', $metadata);
$this->assertArrayHasKey('parameters', $metadata);
$this->assertArrayHasKey('properties', $metadata['parameters']);
$this->assertArrayHasKey('location', $metadata['parameters']['properties']);
}
/** @test */
public function it_handles_valid_location()
{
$result = $this->weatherTool->handle('San Francisco');
$this->assertIsArray($result);
$this->assertArrayHasKey('location', $result);
$this->assertArrayHasKey('temperature', $result);
$this->assertArrayHasKey('conditions', $result);
$this->assertEquals('San Francisco', $result['location']);
}
/** @test */
public function it_throws_exception_for_invalid_location()
{
$this->expectException(InvalidToolParameters::class);
$this->weatherTool->handle('NonExistentLocation123456789');
}
/** @test */
public function it_is_registered_with_the_agency()
{
$tool = $this->agency->getTool('weather');
$this->assertNotNull($tool);
$this->assertInstanceOf(WeatherTool::class, $tool);
}
}
模拟外部服务
通过模拟 HTTP 响应来测试与外部服务交互的工具:
<?php
namespace Tests\Unit\Tools;
use Tests\TestCase;
use App\Tools\ExternalApiTool;
use Illuminate\Support\Facades\Http;
use ProjectSaturnStudios\Vibes\Exceptions\ToolExecutionError;
class ExternalApiToolTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// 模拟外部 API 响应
Http::fake([
'api.example.com/data*' => Http::response([
'success' => true,
'data' => [
'id' => 123,
'name' => 'Test Item',
'value' => 99.95
]
], 200),
'api.example.com/error*' => Http::response([
'success' => false,
'error' => 'Resource not found'
], 404),
]);
}
/** @test */
public function it_fetches_and_formats_external_data()
{
$tool = new ExternalApiTool();
$result = $tool->handle('data', ['id' => 123]);
$this->assertIsArray($result);
$this->assertTrue($result['success']);
$this->assertEquals('Test Item', $result['data']['name']);
$this->assertEquals(99.95, $result['data']['value']);
}
/** @test */
public function it_handles_api_errors_appropriately()
{
$this->expectException(ToolExecutionError::class);
$tool = new ExternalApiTool();
$tool->handle('error', ['id' => 999]);
}
/** @test */
public function it_retries_on_temporary_failures()
{
Http::fake([
// 第一次调用失败,第二次成功
'api.example.com/flaky*' => Http::sequence()
->push(['error' => 'Server busy'], 503)
->push(['success' => true, 'data' => ['name' => 'Flaky Test']], 200),
]);
$tool = new ExternalApiTool();
$result = $tool->handle('flaky', ['id' => 456]);
$this->assertTrue($result['success']);
$this->assertEquals('Flaky Test', $result['data']['name']);
}
}
与 TheAgency 进行集成测试
测试工具与 TheAgency 的集成:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Tools\CalculatorTool;
use ProjectSaturnStudios\Vibes\TheAgency;
use ProjectSaturnStudios\Vibes\VibeSesh;
class AgencyToolIntegrationTest extends TestCase
{
protected TheAgency $agency;
protected function setUp(): void
{
parent::setUp();
$this->agency = app(TheAgency::class);
// 注册测试工具
$this->agency->addTool(CalculatorTool::class);
}
/** @test */
public function it_executes_calculator_tool_through_agency()
{
// 创建一个测试会话
$session = new VibeSesh('test-session-123');
// 准备一个工具执行请求
$request = [
'jsonrpc' => '2.0',
'method' => 'run_tool',
'id' => 'req-'.time(),
'session_id' => $session->getId(),
'params' => [
'name' => 'calculator',
'input' => [
'operation' => 'add',
'a' => 5,
'b' => 7
]
]
];
// 通过 TheAgency 处理
$response = $this->agency->processRequest($request, $session);
// 验证响应
$this->assertEquals('2.0', $response['jsonrpc']);
$this->assertEquals($request['id'], $response['id']);
$this->assertEquals(12, $response['result']['sum']);
}
/** @test */
public function it_returns_proper_error_for_invalid_tool_name()
{
$session = new VibeSesh('test-session-123');
$request = [
'jsonrpc' => '2.0',
'method' => 'run_tool',
'id' => 'req-'.time(),
'session_id' => $session->getId(),
'params' => [
'name' => 'non-existent-tool',
'input' => []
]
];
$response = $this->agency->processRequest($request, $session);
$this->assertArrayHasKey('error', $response);
$this->assertEquals(-32601, $response['error']['code']);
}
}
测试 SSE 连接
使用以下方法测试服务器发送事件 (SSE) 连接:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Testing\TestResponse;
use Illuminate\Support\Facades\Event;
use App\Events\AgentConnected;
class SSEConnectionTest extends TestCase
{
/** @test */
public function it_establishes_sse_connection_successfully()
{
// 监听连接事件
Event::fake([AgentConnected::class]);
// 向 SSE 端点发出请求
$response = $this->get('/mcp/sse', [
'Accept' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Requested-With' => 'XMLHttpRequest',
]);
// 检查连接设置
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'text/event-stream');
$response->assertHeader('Cache-Control', 'no-cache, private');
$response->assertHeader('Connection', 'keep-alive');
// 确认响应具有 SSE 格式
$this->assertStringContainsString('data:', $response->getContent());
$this->assertStringContainsString('id:', $response->getContent());
$this->assertStringContainsString('event: endpoint-info', $response->getContent());
// 验证事件是否已分派
Event::assertDispatched(AgentConnected::class);
}
/** @test */
public function it_includes_endpoint_information_in_sse_init()
{
$response = $this->get('/mcp/sse');
// 从 SSE 响应中提取 JSON 数据
$matches = [];
preg_match('/data: (.+)/', $response->getContent(), $matches);
if (count($matches) > 1) {
$data = json_decode($matches[1], true);
$this->assertIsArray($data);
$this->assertArrayHasKey('endpoints', $data);
$this->assertArrayHasKey('message', $data['endpoints']);
$this->assertEquals(route('vibes.messages'), $data['endpoints']['message']);
} else {
$this->fail('Could not extract SSE data from response');
}
}
}
模拟 AI 代理交互
测试与 AI 代理交互的组件:
<?php
namespace Tests\Unit\Services;
use Tests\TestCase;
use App\Services\ClaudeService;
use Illuminate\Support\Facades\Http;
use ProjectSaturnStudios\Vibes\TheAgency;
use App\Tools\CalculatorTool;
class ClaudeServiceTest extends TestCase
{
protected ClaudeService $service;
protected function setUp(): void
{
parent::setUp();
// 获取 TheAgency 并注册工具
$agency = app(TheAgency::class);
$agency->addTool(CalculatorTool::class);
// 使用模拟的依赖项创建服务
$this->service = app(ClaudeService::class);
// 模拟 Claude API 响应
Http::fake([
'https://api.anthropic.com/v1/messages' => Http::response([
'id' => 'msg_01234567',
'model' => 'claude-3-opus-20240229',
'content' => [
['type' => 'text', 'text' => 'This is a test response from Claude']
],
'tool_calls' => [
[
'id' => 'call_01234567',
'name' => 'calculator',
'parameters' => [
'operation' => 'multiply',
'a' => 4,
'b' => 5
]
]
]
], 200),
]);
}
/** @test */
public function it_sends_request_to_claude_api()
{
$response = $this->service->createMessage('Test prompt');
$this->assertIsArray($response);
$this->assertArrayHasKey('content', $response);
$this->assertArrayHasKey('tool_calls', $response);
$this->assertEquals('This is a test response from Claude', $response['content'][0]['text']);
}
/** @test */
public function it_handles_tool_calls()
{
// 从 API 响应中获取一个示例工具调用
$response = $this->service->createMessage('Test prompt');
$toolCall = $response['tool_calls'][0];
// 处理工具调用
$result = $this->service->handleToolCall($toolCall, 'test-convo-123');
$this->assertIsArray($result);
$this->assertEquals('success', $result['status']);
$this->assertEquals(20, $result['result']['product']); // 4 * 5 = 20
}
}
PHPUnit 配置建议
以下是一个示例 PHPUnit 配置,用于优化 Laravel Vibes 组件的测试:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Tools">
<directory suffix="Test.php">./tests/Unit/Tools</directory>
</testsuite>
<testsuite name="Agent">
<directory suffix="Test.php">./tests/Feature/Agent</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="ANTHROPIC_API_KEY" value="test_api_key"/>
<env name="VIBES_TOOLS_CACHE" value="false"/>
</php>
</phpunit>
测试最佳实践
测试 Laravel Vibes 组件时,请遵循以下最佳实践:
- 隔离你的测试:确保每个测试都是独立的,并且可以按任何顺序运行。
- 使用测试替身:模拟外部依赖项,如 HTTP API、数据库和服务。
- 测试边缘情况:验证无效输入、错误条件和边界值的行为。
- 模拟真实世界场景:创建反映实际使用模式的测试。
- 测试异步行为:验证 SSE 连接和事件侦听器是否正常工作。
- 使用数据提供程序:使用多个输入/输出组合测试工具。
- 检查异常处理:验证工具是否为无效输入引发适当的异常。
- 独立测试中间件:将中间件功能与控制器逻辑隔离。
有关详细的示例测试,请查看 Laravel Vibes 存储库中的 tests 目录。
👉 了解更多: 有关更详细的测试示例和策略,请参阅 UsageExamples.md 中的测试部分。
🔄 Laravel 12 兼容性
Laravel Vibes 旨在与 Laravel 12.x 完全兼容,利用其许多新特性和改进来提供最佳性能和开发者体验。
版本兼容性矩阵
下表显示了 Laravel Vibes 扩展包版本与 Laravel 框架版本的兼容性:
Laravel Vibes 版本 | Laravel 版本 | PHP 版本 | 状态 |
---|---|---|---|
1.0.x | 12.x | ≥ 8.2 | 完全支持 |
0.9.x | 11.x | ≥ 8.1 | 维护 |
0.8.x | 10.x | ≥ 8.1 | 遗留支持 |
0.7.x | 9.x | ≥ 8.0 | 停止支持 |
利用的 Laravel 12 特性
Laravel Vibes 利用了 Laravel 12 中引入的几个新特性:
改进的速率限制
Laravel Vibes 使用 Laravel 12 中增强的速率限制功能来管理 AI 代理连接并防止资源滥用:
// 在 LaravelVibesServiceProvider.php 中
RateLimiter::for('mcp-agent', function (Request $request) {
return [
Limit::perMinute(60)->by($request->session()->getId()),
Limit::perDay(1000)->by($request->session()->getId()),
];
});
路由分组改进
该扩展包使用 Laravel 12 改进的路由组定义来实现更
推荐服务器
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
MCP Package Docs Server
促进大型语言模型高效访问和获取 Go、Python 和 NPM 包的结构化文档,通过多语言支持和性能优化来增强软件开发。
Claude Code MCP
一个实现了 Claude Code 作为模型上下文协议(Model Context Protocol, MCP)服务器的方案,它可以通过标准化的 MCP 接口来使用 Claude 的软件工程能力(代码生成、编辑、审查和文件操作)。
@kazuph/mcp-taskmanager
用于任务管理的模型上下文协议服务器。它允许 Claude Desktop(或任何 MCP 客户端)在基于队列的系统中管理和执行任务。
mermaid-mcp-server
一个模型上下文协议 (MCP) 服务器,用于将 Mermaid 图表转换为 PNG 图像。
Jira-Context-MCP
MCP 服务器向 AI 编码助手(如 Cursor)提供 Jira 工单信息。

Linear MCP Server
一个模型上下文协议(Model Context Protocol)服务器,它与 Linear 的问题跟踪系统集成,允许大型语言模型(LLM)通过自然语言交互来创建、更新、搜索和评论 Linear 问题。

Sequential Thinking MCP Server
这个服务器通过将复杂问题分解为顺序步骤来促进结构化的问题解决,支持修订,并通过完整的 MCP 集成来实现多条解决方案路径。
Curri MCP Server
通过管理文本笔记、提供笔记创建工具以及使用结构化提示生成摘要,从而实现与 Curri API 的交互。