使用 Laravel Workflow 作为 MCP 工具提供给 AI 客户端
Model Context Protocol (MCP) 正在迅速成为 AI 助手(如 Claude、ChatGPT 和 GitHub Copilot)与外部工具和服务交互的标准方式。通过 Laravel MCP,你可以将 Laravel Workflow 流程暴露为可调用的工具,任何兼容 MCP 的 AI 客户端都可以发现、调用和监控这些工具。
在本文中,我们将展示如何构建一个 MCP 服务器,使 AI 客户端能够:
- 发现可用的工作流
- 异步启动工作流
- 轮询状态并检索结果
这创建了一个强大的模式,AI 代理可以编排长时间运行的持久化工作流,非常适合无法在单个请求中完成的复杂任务。
原文链接 使用 Laravel Workflow 作为 MCP 工具提供给 AI 客户端
为什么选择 MCP + Laravel Workflow?
Laravel Workflow 擅长持久化、有状态的执行。MCP 擅长为 AI 客户端提供对外部能力的结构化访问。两者结合可以实现:
- 异步 AI 操作:启动工作流,继续对话,稍后检查结果
- 可靠执行:工作流可以在崩溃、重试和长时间等待中存活
- 可观察性:通过 Waterline 的仪表板跟踪每个工作流
- 无状态服务器:MCP 服务器不保存状态。客户端跟踪工作流 ID
这类似于人类委派任务的方式:"开始这个报告,我稍后再检查。"
我们要构建什么
我们将创建一个包含三个工具的 MCP 服务器:
| 工具 | 用途 |
|---|---|
| list_workflows | 发现可用的工作流并查看最近的运行记录 |
| start_workflow | 启动工作流并获取跟踪 ID |
| get_workflow_result | 检查状态并在完成时检索输出 |
分步实现
安装 Laravel MCP
bash
composer require laravel/mcp
php artisan vendor:publish --tag=ai-routes
这会为你提供 routes/ai.php,你将在其中注册 MCP 服务器。
创建 MCP 服务器
bash
php artisan make:mcp-server WorkflowServer
为 AI 配置说明:
php
namespace App\Mcp\Servers;
use App\Mcp\Tools\GetWorkflowResultTool;
use App\Mcp\Tools\ListWorkflowsTool;
use App\Mcp\Tools\StartWorkflowTool;
use Laravel\Mcp\Server;
class WorkflowServer extends Server
{
protected string $name = 'Laravel Workflow Server';
protected string $version = '1.0.0';
protected string $instructions = <<<'MARKDOWN'
This server allows you to start and monitor Laravel Workflows.
## Typical Usage Pattern
1. Call `list_workflows` to see what workflows are available.
2. Call `start_workflow` with the workflow name and arguments.
3. Store the returned `workflow_id`.
4. Call `get_workflow_result` until status is `WorkflowCompletedStatus`.
5. Read the `output` field for the result.
## Status Values
- `WorkflowCreatedStatus` - Workflow has been created
- `WorkflowPendingStatus` - Queued for execution
- `WorkflowRunningStatus` - Currently executing
- `WorkflowWaitingStatus` - Waiting (timer, signal, etc.)
- `WorkflowCompletedStatus` - Finished successfully
- `WorkflowFailedStatus` - Encountered an error
MARKDOWN;
protected array $tools = [
ListWorkflowsTool::class,
StartWorkflowTool::class,
GetWorkflowResultTool::class,
];
}
创建启动工作流工具
bash
php artisan make:mcp-tool StartWorkflowTool
php
namespace App\Mcp\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Arr;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
use Workflow\Workflow;
use Workflow\WorkflowStub;
class StartWorkflowTool extends Tool
{
protected string $description = <<<'MARKDOWN'
Start a Laravel Workflow asynchronously and return its workflow ID.
The workflow will execute in the background on the queue. Use the
`get_workflow_result` tool to poll for status and retrieve results
once the workflow completes.
MARKDOWN;
public function handle(Request $request): Response
{
$data = $request->validate([
'workflow' => ['required', 'string'],
'args' => ['nullable', 'array'],
'external_id' => ['nullable', 'string', 'max:255'],
]);
$workflowKey = $data['workflow'];
$args = Arr::get($data, 'args', []);
$externalId = $data['external_id'] ?? null;
$workflowClass = $this->resolveWorkflowClass($workflowKey);
if ($workflowClass === null) {
return Response::error("Unknown workflow: {$workflowKey}");
}
if (! class_exists($workflowClass) || ! is_subclass_of($workflowClass, Workflow::class)) {
return Response::error("Invalid workflow class: {$workflowClass}");
}
$stub = WorkflowStub::make($workflowClass);
$stub->start(...array_values($args));
$status = $stub->status();
$statusName = is_object($status) ? class_basename($status) : class_basename((string) $status);
return Response::json([
'workflow_id' => $stub->id(),
'workflow' => $workflowKey,
'status' => $statusName,
'external_id' => $externalId,
'message' => 'Workflow started. Use get_workflow_result to poll status.',
]);
}
protected function resolveWorkflowClass(string $key): ?string
{
$mapped = config("workflow_mcp.workflows.{$key}");
if ($mapped !== null) {
return $mapped;
}
if (config('workflow_mcp.allow_fqcn', false) && str_contains($key, '\\')) {
return $key;
}
return null;
}
public function schema(JsonSchema $schema): array
{
$workflows = implode(', ', array_keys(config('workflow_mcp.workflows', [])));
return [
'workflow' => $schema->string()
->description("The workflow to start. Available: {$workflows}"),
'args' => $schema->object()
->description('Arguments for the workflow execute() method.'),
'external_id' => $schema->string()
->description('Optional idempotency key for tracking.'),
];
}
}
创建获取结果工具
bash
php artisan make:mcp-tool GetWorkflowResultTool
php
namespace App\Mcp\Tools;
use App\Models\StoredWorkflow;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
use Workflow\States\WorkflowCompletedStatus;
use Workflow\States\WorkflowFailedStatus;
use Workflow\WorkflowStub;
class GetWorkflowResultTool extends Tool
{
protected string $description = <<<'MARKDOWN'
Fetch the status and, if completed, the output of a Laravel Workflow.
Use the workflow_id returned by `start_workflow` to check progress.
Once status is `WorkflowCompletedStatus`, the output field contains the result.
MARKDOWN;
public function handle(Request $request): Response
{
$data = $request->validate([
'workflow_id' => ['required'],
]);
$workflowId = $data['workflow_id'];
$stored = StoredWorkflow::find($workflowId);
if (! $stored) {
return Response::json([
'found' => false,
'message' => "Workflow {$workflowId} not found.",
]);
}
$workflow = WorkflowStub::load($workflowId);
$status = $workflow->status();
$statusName = is_object($status) ? class_basename($status) : class_basename((string) $status);
$running = $workflow->running();
$result = null;
$error = null;
if (! $running && str_contains($statusName, 'Completed')) {
$result = $workflow->output();
}
if (! $running && str_contains($statusName, 'Failed')) {
$exception = $stored->exceptions()->latest()->first();
$error = $exception?->exception ?? 'Unknown error';
}
return Response::json([
'found' => true,
'workflow_id' => $workflowId,
'status' => $statusName,
'running' => $running,
'output' => $result,
'error' => $error,
'created_at' => $stored->created_at?->toIso8601String(),
'updated_at' => $stored->updated_at?->toIso8601String(),
]);
}
public function schema(JsonSchema $schema): array
{
return [
'workflow_id' => $schema->string()
->description('The workflow ID returned by start_workflow.'),
];
}
}
创建列表工作流工具
bash
php artisan make:mcp-tool ListWorkflowsTool
php
namespace App\Mcp\Tools;
use App\Models\StoredWorkflow;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class ListWorkflowsTool extends Tool
{
protected string $description = <<<'MARKDOWN'
List available workflow types and optionally show recent workflow runs.
Use this to discover what workflows can be started, or to see
the status of recent executions.
MARKDOWN;
public function handle(Request $request): Response
{
$data = $request->validate([
'show_recent' => ['nullable', 'boolean'],
'limit' => ['nullable', 'integer', 'min:1', 'max:50'],
'status' => ['nullable', 'string'],
]);
$showRecent = $data['show_recent'] ?? false;
$limit = $data['limit'] ?? 10;
$statusFilter = $data['status'] ?? null;
$availableWorkflows = [];
foreach (config('workflow_mcp.workflows', []) as $key => $class) {
$availableWorkflows[] = ['key' => $key, 'class' => $class];
}
$response = [
'available_workflows' => $availableWorkflows,
];
if ($showRecent) {
$query = StoredWorkflow::query()
->orderBy('created_at', 'desc')
->limit($limit);
if ($statusFilter) {
$query->where('status', 'like', "%{$statusFilter}%");
}
$response['recent_workflows'] = $query->get()->map(function ($w) {
$status = $w->status;
$statusName = is_object($status) ? class_basename($status) : class_basename((string) $status);
return [
'id' => $w->id,
'class' => $w->class,
'status' => $statusName,
'created_at' => $w->created_at?->toIso8601String(),
];
});
}
return Response::json($response);
}
public function schema(JsonSchema $schema): array
{
return [
'show_recent' => $schema->boolean()
->description('Include recent workflow runs in response.'),
'limit' => $schema->integer()
->description('Max recent workflows to return (default: 10).'),
'status' => $schema->string()
->description('Filter by status (e.g., "Completed", "Failed").'),
];
}
}
配置可用的工作流
创建 config/workflow_mcp.php 来将 AI 客户端可以启动的工作流加入白名单:
php
return [
'allow_fqcn' => env('WORKFLOW_MCP_ALLOW_FQCN', false),
'workflows' => [
'simple' => App\Workflows\Simple\SimpleWorkflow::class,
'prism' => App\Workflows\Prism\PrismWorkflow::class,
],
];
这可以防止任意类执行。只有映射的工作流可以访问。
注册 MCP 服务器
更新 routes/ai.php:
php
use App\Mcp\Servers\WorkflowServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('/mcp/workflows', WorkflowServer::class);
连接 AI 客户端
VS Code / GitHub Copilot
在项目中创建 .vscode/mcp.json:
json
{
"servers": {
"laravel-workflow": {
"type": "http",
"url": "http://localhost/mcp/workflows"
}
}
}
此配置适用于本地开发和 GitHub Codespaces。在 Codespaces 中,VS Code 服务器在容器内运行,因此 localhost 可以正确访问 Laravel 服务器,无需公共端口或 *.app.github.dev URL。
重新加载 VS Code(Cmd/Ctrl+Shift+P → "Developer: Reload Window")后,Copilot 可以直接在聊天中使用工作流工具。
实际使用
连接后,你可以与 AI 助手进行自然对话:
你:"有哪些工作流可用?"
AI :调用 list_workflows "我找到了 2 个工作流:simple 和 prism。"
你:"启动 prism 工作流"
AI :调用 start_workflow "已启动工作流 ID 42。我会检查它的状态。"
AI :调用 get_workflow_result "工作流已完成!这是生成的用户配置文件:{ name: 'Elena', hobbies: [...] }"
这创造了一种无缝体验,AI 助手可以编排复杂的长时间运行的操作,同时让用户了解情况。
这种模式的强大之处
- 持久化:工作流可以在服务器重启和网络故障中存活
- 异步设计:AI 客户端不会阻塞等待完成
- 可观察:每个工作流都在 Waterline 的仪表板中跟踪
- 安全:基于白名单的工作流访问防止任意执行
- 无状态 MCP:服务器不保存状态。客户端跟踪工作流 ID
在浏览器中立即试用
此 MCP 集成已包含并预配置在 Laravel Workflow Sample App 中。
要试用:
- 在 GitHub 上打开 sample-app 仓库
- 点击 Code → Codespaces → Create codespace on main
- 等待环境构建
- 设置应用并启动队列工作器:
bash
php artisan app:init
php artisan queue:work
- 启用 Laravel Workflow Server MCP 工具
- 让你的 AI 列出并运行工作流!
接下来可以做什么
你可以扩展此模式以实现:
- 参数化工作流:将用户输入传递给工作流参数
- Webhook 通知:推送完成事件而不是轮询
- 工作流信号:让 AI 客户端向等待的工作流发送信号
- 进度流式传输:使用 SSE 实时流式传输工作流进度
- 多步骤代理:在对话中将多个工作流链接在一起
Laravel Workflow 的持久化执行与 MCP 的工具协议相结合,为真正有能力的 AI 代理创建了一个基础,这些代理可以处理现实世界的复杂性。