夜科贡渍为什么选择 MCP + Laravel Workflow?
Laravel Workflow 擅长持久化、有状态的执行。MCP 擅长为 AI 客户端提供对外部能力的结构化访问。两者结合可以实现:
异步 AI 操作:启动工作流,继续对话,稍后检查结果
可靠执行:工作流可以在崩溃、重试和长时间等待中存活
可观察性:通过 Waterline 的仪表板跟踪每个工作流
无状态服务器:MCP 服务器不保存状态。客户端跟踪工作流 ID
这类似于人类委派任务的方式:"开始这个报告,我稍后再检查。"
我们要构建什么
我们将创建一个包含三个工具的 MCP 服务器:
工具 用途
list_workflows 发现可用的工作流并查看最近的运行记录
start_workflow 启动工作流并获取跟踪 ID
get_workflow_result 检查状态并在完成时检索输出
分步实现
安装 Laravel MCP
composer require laravel/mcp
php artisan vendor:publish --tag=ai-routes
这会为你提供 routes/ai.php,你将在其中注册 MCP 服务器。
创建 MCP 服务器
php artisan make:mcp-server WorkflowServer
为 AI 配置说明:
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
-
Call `list_workflows` to see what workflows are available.
-
Call `start_workflow` with the workflow name and arguments.
-
Store the returned `workflow_id`.
-
Call `get_workflow_result` until status is `WorkflowCompletedStatus`.
-
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,
];
}
创建启动工作流工具
php artisan make:mcp-tool StartWorkflowTool
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.'),
];
}
}
创建获取结果工具
php artisan make:mcp-tool GetWorkflowResultTool
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.'),
];
}
}
创建列表工作流工具
php artisan make:mcp-tool ListWorkflowsTool
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 客户端可以启动的工作流加入白名单:
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:
use App\Mcp\Servers\WorkflowServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('/mcp/workflows', WorkflowServer::class);
连接 AI 客户端
VS Code / GitHub Copilot
在项目中创建 .vscode/mcp.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
等待环境构建
设置应用并启动队列工作器:
php artisan app:init
php artisan queue:work
启用 Laravel Workflow Server MCP 工具
让你的 AI 列出并运行工作流!
接下来可以做什么
你可以扩展此模式以实现:
参数化工作流:将用户输入传递给工作流参数
Webhook 通知:推送完成事件而不是轮询
工作流信号:让 AI 客户端向等待的工作流发送信号
进度流式传输:使用 SSE 实时流式传输工作流进度
多步骤代理:在对话中将多个工作流链接在一起