关注【索引目录】服务号,更多精彩内容等你来探索!
简介:MCP 为何改变一切
说实话,当我第一次听说模型上下文协议 (MCP) 时,我心存疑虑。这又是一个协议?又一个让 AI 与应用交互的方式?但在构建了我的第一个 MCP 服务器后,我突然明白了。这不仅仅是另一个 API 标准。它是一种从根本上改变 AI 助手如何与数据交互的方式。
事实是这样的:我们一直在构建 REST API、GraphQL 端点和 Webhook。但 MCP 与众不同。它专为 AI 代理而设计,能够发现和使用您应用的功能。您无需编写 AI 可能误解的文档,而是为 AI 提供可以自信调用的结构化工具。这就像给某人提供书面路线和 GPS 导航之间的区别。
我们正在构建什么
在本教程中,我们将构建一个任务管理 MCP 服务器。为什么要使用任务?因为它是每个人都能理解的,但它也演示了所有关键的 MCP 概念:
- 工具
——AI 可以执行的操作(创建任务、将其标记为完成) - 资源
——人工智能可以读取的数据(任务统计数据、报告) - 提示
- 帮助 AI 更有效地交互的模板
最终,您将拥有一个可以运行的 MCP 服务器,让 Claude(或任何 MCP 客户端)以对话方式管理您的任务。想象一下,只需输入“创建一个任务来审核第四季度报告”,它就会自动运行。这就是我们正在构建的。
让我们开始吧。
第 1 部分:设置
安装 Laravel
首先,让我们创建一个新的 Laravel 项目并运行。我假设你已经安装了 Composer:
composer create-project laravel/laravel task-mcp-server
cd task-mcp-server
安装 Laravel MCP
现在,魔法来了。Laravel 的 MCP 包让整个过程变得异常优雅:
composer require laravel/mcp
安装完成后,发布 MCP 路由文件:
php artisan vendor:publish --tag=ai-routes
这会创建一个routes/ai.php文件——你可以把它想象成你的文件routes/web.php,但专门用于 AI 交互。很酷吧?
数据库设置
我们需要一个地方来存储我们的任务。.env使用你的数据库凭证更新你的文件:
DB_CONNECTION=sqlite
对于本教程,我使用 SQLite 来使事情变得简单,但 Laravel 支持的任何数据库都可以正常工作。
创建任务模型和迁移
让我们通过迁移来创建我们的任务模型:
php artisan make:model Task -m
打开迁移文件database/migrations/*_create_tasks_table.php并定义我们的模式:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
$table->enum('priority', ['low', 'medium', 'high'])->default('medium');
$table->boolean('completed')->default(false);
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tasks');
}
};
现在更新app/Models/Task.php模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = [
'title',
'description',
'priority',
'completed',
'completed_at',
];
protected $casts = [
'completed' => 'boolean',
'completed_at' => 'datetime',
];
public function scopeIncomplete($query)
{
return $query->where('completed', false);
}
public function scopeCompleted($query)
{
return $query->where('completed', true);
}
public function markComplete(): void
{
$this->update([
'completed' => true,
'completed_at' => now(),
]);
}
}
运行迁移:
php artisan migrate
创建自定义 Eloquent Builder
在开始构建 MCP 服务器之前,我们先来准备一个自定义的 Eloquent Builder。这个模式乍一看可能有点儿大材小用,但一旦掌握了它,你就会惊叹以前没有它的时候该如何生活。
问题在于:随着我们构建 MCP 工具和资源,我们不得不一遍又一遍地编写相同的查询逻辑。“获取未完成的任务”、“按优先级获取任务”、“按关键字搜索任务”。我们可以在模型上使用作用域,但这会使模型类变得混乱。我们可以内联编写查询,但这会导致重复。
解决方案?一个自定义构建器类,它将我们所有的查询逻辑封装在一个干净、可测试、可重用的地方。
创建一个新目录app/Models/Builders并添加TaskBuilder.php:
<?php
namespace App\Models\Builders;
use Illuminate\Database\Eloquent\Builder;
class TaskBuilder extends Builder
{
public function forUser(int $userId): self
{
return $this->where('user_id', $userId);
}
public function incomplete(): self
{
return $this->where('completed', false);
}
public function completed(): self
{
return $this->where('completed', true);
}
public function priority(string $priority): self
{
return $this->where('priority', $priority);
}
public function search(?string $keyword): self
{
if (empty($keyword)) {
return $this;
}
return $this->where(function ($query) use ($keyword) {
$query->where('title', 'like', "%{$keyword}%")
->orWhere('description', 'like', "%{$keyword}%");
});
}
public function highPriority(): self
{
return $this->where('priority', 'high');
}
public function recentlyCompleted(int $days = 30): self
{
return $this->completed()
->where('completed_at', '>=', now()->subDays($days));
}
public function createdInPeriod(int $days): self
{
return $this->where('created_at', '>=', now()->subDays($days));
}
public function orderByPriority(): self
{
return $this->orderByRaw("FIELD(priority, 'high', 'medium', 'low')");
}
}
现在更新Task模型以使用此构建器:
<?php
namespace App\Models;
use App\Builders\TaskBuilder;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = [
'user_id',
'title',
'description',
'priority',
'completed',
'completed_at',
];
protected $casts = [
'completed' => 'boolean',
'completed_at' => 'datetime',
];
/**
* Create a new Eloquent query builder for the model.
*/
public function newEloquentBuilder($query): TaskBuilder
{
return new TaskBuilder($query);
}
public function markComplete(): void
{
$this->update([
'completed' => true,
'completed_at' => now(),
]);
}
}
瞧瞧。我们的模型现在简洁明了,并且专注于它应该做的事情:定义数据结构和基本模型方法。所有查询逻辑都位于构建器中。
这种方法的美妙之处在于:无论在哪里使用Task::query(),我们都可以访问这些流畅、可链接的方法。无需再这样写:
Task::where('user_id', $userId)
->where('completed', false)
->where('priority', 'high')
->get();
我们可以这样写:
Task::forUser($userId)
->incomplete()
->highPriority()
->get();
它更易读、更易于维护、也更易于测试。此外,如果我们需要更改优先级过滤方式或“未完成”的含义,只需在一个地方进行更改即可。
现在开始动手吧!我们有一个 Laravel 应用,它包含一个 Task 模型和一个强大的自定义构建器。接下来,让我们来构建我们的 MCP 服务器。
第 2 部分:创建您的第一个 MCP 服务器
生成服务器
Laravel MCP 提供了一个 Artisan 命令来搭建服务器:
php artisan make:mcp-server TaskServer
这将创建app/Mcp/Servers/TaskServer.php。打开它,你会看到一个干净的空服务器,可供我们配置:
<?php
namespace App\Mcp\Servers;
use Laravel\Mcp\Server;
class TaskServer extends Server
{
protected string $name = 'Task Server';
protected string $version = '0.0.1';
protected string $instructions = <<<'MARKDOWN'
Instructions describing how to use the server and its features.
MARKDOWN;
protected array $tools = [];
protected array $resources = [];
protected array $prompts = [];
}
简洁之美。我们将在构建工具、资源和提示时填充这些数组。
注册服务器
打开routes/ai.php并注册您的服务器:
<?php
use App\Mcp\Servers\TaskServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('/mcp/tasks', TaskServer::class);
就这样。您的 MCP 服务器现在可以在 访问了/mcp/tasks。当 AI 客户端连接到此端点时,它将发现我们即将创建的所有工具、资源和提示。
第 3 部分:构建您的第一个工具
有趣的地方就在这里。工具是 AI 可以通过你的服务器执行的操作。让我们从最基本的开始:创建任务。
生成 CreateTask 工具
php artisan make:mcp-tool CreateTaskTool
这将创建app/Mcp/Tools/CreateTaskTool.php。现在让我们正确地构建它:
<?php
namespace App\Mcp\Tools;
use App\Models\Task;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class CreateTaskTool extends Tool
{
protected string $description = 'Creates a new task with a title, optional description, and priority level.';
public function handle(Request $request): Response
{
// Validate the input with clear, helpful error messages
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'priority' => ['sometimes', 'in:low,medium,high'],
], [
'title.required' => 'You must provide a task title. For example: "Review Q4 report" or "Call John about project".',
'title.max' => 'Task title is too long. Please keep it under 255 characters.',
'description.max' => 'Task description is too long. Please keep it under 1000 characters.',
'priority.in' => 'Priority must be one of: low, medium, or high.',
]);
// Create the task
$task = Task::create($validated);
// Return a clear, informative response
return Response::text(
"✅ Task created successfully!\n\n" .
"**{$task->title}**\n" .
($task->description ? "{$task->description}\n" : '') .
"Priority: {$task->priority}\n" .
"ID: {$task->id}"
);
}
public function schema(JsonSchema $schema): array
{
return [
'title' => $schema->string()
->description('The title of the task (required)')
->required(),
'description' => $schema->string()
->description('Optional detailed description of the task'),
'priority' => $schema->enum(['low', 'medium', 'high'])
->description('Priority level for the task')
->default('medium'),
];
}
}
让我来分析一下这里发生的事情,因为发生的事情比你想象的要多。
Schema 方法:我们通过这种方法告诉 AI 这个工具接受哪些输入。可以把它想象成一份合约。AI 知道它需要提供一个title,也可以选择提供一个description,并且如果提供了一个priority,它必须是这三个值之一。这种方法非常强大,因为 AI 甚至可以在调用工具之前就推断出这些约束条件。
使用上下文进行验证:请注意,我不仅仅是在进行验证,我还提供了有用的错误消息。当 AI 收到验证错误时,它可以使用这些信息以更好的输入进行重试。这对于良好的用户体验至关重要。
清晰的响应:响应不仅仅是“任务已创建”。它会提供详细的确认信息。AI 可以自然地将这些信息传达给用户。
注册工具
更新您的TaskServer以包含此工具:
<?php
namespace App\Mcp\Servers;
use App\Mcp\Tools\CreateTaskTool;
use Laravel\Mcp\Server;
class TaskServer extends Server
{
protected array $tools = [
CreateTaskTool::class,
];
protected array $resources = [];
protected array $prompts = [];
}
测试您的工具
让我们确保它能正常工作。Laravel MCP 包含一个非常棒的测试 API,我们将使用 Pest PHP,因为说实话,一旦你用了 Pest,就再也不想用了。它的语法非常简洁。
如果您尚未安装 Pest:
composer require pestphp/pest --dev --with-all-dependencies
php artisan pest:install
创建测试文件tests/Feature/TaskMcpTest.php:
<?php
use App\Mcp\Servers\TaskServer;
use App\Mcp\Tools\CreateTaskTool;
use App\Models\Task;
uses()->group('mcp');
test('can create a task with all fields', function () {
$response = TaskServer::tool(CreateTaskTool::class, [
'title' => 'Write tutorial',
'description' => 'Complete the Laravel MCP tutorial',
'priority' => 'high',
]);
$response->assertOk();
$response->assertSee('Write tutorial');
$response->assertSee('high');
expect(Task::first())
->title->toBe('Write tutorial')
->priority->toBe('high')
->completed->toBeFalse();
});
test('task title is required', function () {
$response = TaskServer::tool(CreateTaskTool::class, [
'description' => 'A task without a title',
]);
$response->assertHasErrors();
});
test('invalid priority is rejected', function () {
$response = TaskServer::tool(CreateTaskTool::class, [
'title' => 'Test task',
'priority' => 'super-urgent',
]);
$response->assertHasErrors();
});
test('creates task with default priority when not specified', function () {
$response = TaskServer::tool(CreateTaskTool::class, [
'title' => 'Simple task',
]);
$response->assertOk();
expect(Task::first())
->priority->toBe('medium');
});
看看这多简洁啊!没有类的样板代码,没有$this->,只有纯粹的、富有表现力的测试。这就是我所说的 Pest 的含义——它不会妨碍你,让你专注于真正需要测试的内容。
运行测试:
php artisan test
如果一切顺利,恭喜!您刚刚构建了第一个可运行的 MCP 工具。现在,AI 可以在您的系统中创建任务,并进行全面的验证和错误处理。
第 4 部分:添加更多工具
一个工具很酷,但任务管理器还需要更多功能。让我们添加一些用于完成任务和搜索任务的工具。
完成任务工具
php artisan make:mcp-tool CompleteTaskTool
<?php
namespace App\Mcp\Tools;
use App\Models\Task;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class CompleteTaskTool extends Tool
{
protected string $description = 'Marks a task as completed by its ID.';
public function handle(Request $request): Response
{
$validated = $request->validate([
'task_id' => ['required', 'integer', 'exists:tasks,id'],
], [
'task_id.required' => 'You must specify which task to complete using its ID.',
'task_id.exists' => 'No task found with that ID. Try searching for tasks first to find the correct ID.',
]);
$task = Task::findOrFail($validated['task_id']);
if ($task->completed) {
return Response::text("ℹ️ This task was already completed on {$task->completed_at->format('M j, Y')}.");
}
$task->markComplete();
return Response::text(
"✅ Task completed!\n\n" .
"**{$task->title}**\n" .
"Completed: {$task->completed_at->format('M j, Y \a\t g:i A')}"
);
}
public function schema(JsonSchema $schema): array
{
return [
'task_id' => $schema->integer()
->description('The ID of the task to mark as complete')
->required(),
];
}
}
SearchTasks工具
这个很有趣,因为它返回多条信息:
php artisan make:mcp-tool SearchTasksTool
<?php
namespace App\Mcp\Tools;
use App\Models\Task;
use Illuminate\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Tool;
class SearchTasksTool extends Tool
{
protected string $description = 'Searches for tasks by keyword, status, or priority. Returns matching tasks with their details.';
public function handle(Request $request): Response
{
$validated = $request->validate([
'keyword' => ['nullable', 'string', 'max:100'],
'status' => ['nullable', 'in:completed,incomplete,all'],
'priority' => ['nullable', 'in:low,medium,high'],
'limit' => ['nullable', 'integer', 'min:1', 'max:50'],
]);
// Look at how clean this is with our custom builder!
$query = Task::forUser($request->user()->id)
->search($validated['keyword'] ?? null)
->orderBy('created_at', 'desc');
// Filter by status using our builder methods
$status = $validated['status'] ?? 'all';
if ($status === 'completed') {
$query->completed();
} elseif ($status === 'incomplete') {
$query->incomplete();
}
// Filter by priority
if ($priority = $validated['priority'] ?? null) {
$query->priority($priority);
}
$limit = $validated['limit'] ?? 10;
$tasks = $query->limit($limit)->get();
if ($tasks->isEmpty()) {
return Response::text("No tasks found matching your criteria.");
}
$output = "Found {$tasks->count()} task(s):\n\n";
foreach ($tasks as $task) {
$status = $task->completed ? '✅' : '⏳';
$output .= "{$status} **[{$task->id}]** {$task->title}\n";
if ($task->description) {
$output .= " {$task->description}\n";
}
$output .= " Priority: {$task->priority}";
if ($task->completed) {
$output .= " | Completed: {$task->completed_at->format('M j, Y')}";
}
$output .= "\n\n";
}
return Response::text($output);
}
public function schema(JsonSchema $schema): array
{
return [
'keyword' => $schema->string()
->description('Search for tasks containing this keyword in title or description'),
'status' => $schema->enum(['completed', 'incomplete', 'all'])
->description('Filter by completion status')
->default('all'),
'priority' => $schema->enum(['low', 'medium', 'high'])
->description('Filter by priority level'),
'limit' => $schema->integer()
->description('Maximum number of tasks to return (1-50)')
->default(10),
];
}
}
注册所有工具
更新您的TaskServer:
<?php
namespace App\Mcp\Servers;
use App\Mcp\Tools\CompleteTaskTool;
use App\Mcp\Tools\CreateTaskTool;
use App\Mcp\Tools\SearchTasksTool;
use Laravel\Mcp\Server;
class TaskServer extends Server
{
protected array $tools = [
CreateTaskTool::class,
CompleteTaskTool::class,
SearchTasksTool::class,
];
protected array $resources = [];
protected array $prompts = [];
}
现在,你的 AI 助手可以创建任务、标记完成并进行搜索。这已经非常强大了。
第 5 部分:创建资源
工具对于行动非常有用,但人工智能应该知道的数据呢?这时资源就派上用场了。
资源就像是提供 AI 上下文的只读端点。让我们创建两个资源:一个用于任务统计,一个用于已完成任务的列表。
任务统计资源
php artisan make:mcp-resource TaskStatsResource
<?php
namespace App\Mcp\Resources;
use App\Models\Task;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;
class TaskStatsResource extends Resource
{
protected string $description = 'Provides statistical overview of all tasks including completion rates and priority breakdown.';
protected string $uri = 'tasks://stats';
public function handle(Request $request): Response
{
$userId = $request->user()->id;
// Beautiful, readable queries thanks to our builder
$totalTasks = Task::forUser($userId)->count();
$completedTasks = Task::forUser($userId)->completed()->count();
$incompleteTasks = Task::forUser($userId)->incomplete()->count();
$completionRate = $totalTasks > 0
? round(($completedTasks / $totalTasks) * 100, 1)
: 0;
$priorityBreakdown = Task::forUser($userId)
->incomplete()
->selectRaw('priority, count(*) as count')
->groupBy('priority')
->pluck('count', 'priority')
->toArray();
$stats = "# Task Statistics\n\n";
$stats .= "**Total Tasks:** {$totalTasks}\n";
$stats .= "**Completed:** {$completedTasks}\n";
$stats .= "**Incomplete:** {$incompleteTasks}\n";
$stats .= "**Completion Rate:** {$completionRate}%\n\n";
if (!empty($priorityBreakdown)) {
$stats .= "## Incomplete Tasks by Priority\n";
foreach (['high', 'medium', 'low'] as $priority) {
$count = $priorityBreakdown[$priority] ?? 0;
$stats .= "- **" . ucfirst($priority) . ":** {$count}\n";
}
}
return Response::text($stats);
}
}
最近完成的任务资源
php artisan make:mcp-resource RecentCompletedTasksResource
<?php
namespace App\Mcp\Resources;
use App\Models\Task;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Resource;
class RecentCompletedTasksResource extends Resource
{
protected string $description = 'Shows the 20 most recently completed tasks with completion dates.';
protected string $uri = 'tasks://completed/recent';
public function handle(Request $request): Response
{
// One line, crystal clear intent
$completedTasks = Task::forUser($request->user()->id)
->completed()
->orderBy('completed_at', 'desc')
->limit(20)
->get();
if ($completedTasks->isEmpty()) {
return Response::text("No completed tasks yet.");
}
$output = "# Recently Completed Tasks\n\n";
foreach ($completedTasks as $task) {
$output .= "✅ **{$task->title}**\n";
$output .= " Completed: {$task->completed_at->diffForHumans()}\n";
$output .= " Priority was: {$task->priority}\n\n";
}
return Response::text($output);
}
}
注册资源
更新您的TaskServer:
<?php
namespace App\Mcp\Servers;
use App\Mcp\Resources\RecentCompletedTasksResource;
use App\Mcp\Resources\TaskStatsResource;
use App\Mcp\Tools\CompleteTaskTool;
use App\Mcp\Tools\CreateTaskTool;
use App\Mcp\Tools\SearchTasksTool;
use Laravel\Mcp\Server;
class TaskServer extends Server
{
protected array $tools = [
CreateTaskTool::class,
CompleteTaskTool::class,
SearchTasksTool::class,
];
protected array $resources = [
TaskStatsResource::class,
RecentCompletedTasksResource::class,
];
protected array $prompts = [];
}
我喜欢资源的一点是:AI 可以主动检查它们。当有人问“我的任务进展如何?”时,AI 可以读取 TaskStatsResource 并提供有意义的洞察,而无需调用多个工具。
第 6 部分:使用提示
提示是预先配置的对话模板,可帮助 AI 更有效地与您的系统交互。您可以将其视为已保存的工作流程。
让我们创建一个分析任务完成模式的生产力报告提示:
php artisan make:mcp-prompt ProductivityReportPrompt
<?php
namespace App\Mcp\Prompts;
use App\Models\Task;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;
class ProductivityReportPrompt extends Prompt
{
protected string $description = 'Generates a productivity report analyzing task completion patterns over a specified time period.';
public function handle(Request $request): array
{
$validated = $request->validate([
'days' => ['sometimes', 'integer', 'min:1', 'max:90'],
'tone' => ['sometimes', 'in:formal,casual,encouraging'],
]);
$days = $validated['days'] ?? 7;
$tone = $validated['tone'] ?? 'casual';
$userId = $request->user()->id;
// Look how expressive these queries are!
$completedInPeriod = Task::forUser($userId)
->recentlyCompleted($days)
->count();
$createdInPeriod = Task::forUser($userId)
->createdInPeriod($days)
->count();
$stillIncomplete = Task::forUser($userId)
->incomplete()
->createdInPeriod($days)
->count();
$highPriorityIncomplete = Task::forUser($userId)
->incomplete()
->highPriority()
->count();
// Build context for the AI
$context = "# Productivity Data ({$days} days)\n\n";
$context .= "- Tasks completed: {$completedInPeriod}\n";
$context .= "- Tasks created: {$createdInPeriod}\n";
$context .= "- Still incomplete from this period: {$stillIncomplete}\n";
$context .= "- High priority tasks pending: {$highPriorityIncomplete}\n";
// Tone-specific instructions
$toneInstructions = match($tone) {
'formal' => 'Provide a professional, data-driven analysis suitable for a workplace report.',
'encouraging' => 'Be motivating and positive, celebrating accomplishments and gently encouraging progress on pending tasks.',
default => 'Be friendly and conversational, like a helpful colleague.',
};
return [
Response::text(
"You are a productivity analyst. Based on the following task data, " .
"provide insights about the user's productivity. {$toneInstructions}\n\n" .
"{$context}"
)->asAssistant(),
Response::text(
"Please analyze my productivity over the last {$days} days and give me insights."
),
];
}
public function arguments(): array
{
return [
new Argument(
name: 'days',
description: 'Number of days to analyze (1-90)',
required: false
),
new Argument(
name: 'tone',
description: 'Tone for the report: formal, casual, or encouraging',
required: false
),
];
}
}
注册提示
更新您的TaskServer:
<?php
namespace App\Mcp\Servers;
use App\Mcp\Prompts\ProductivityReportPrompt;
use App\Mcp\Resources\RecentCompletedTasksResource;
use App\Mcp\Resources\TaskStatsResource;
use App\Mcp\Tools\CompleteTaskTool;
use App\Mcp\Tools\CreateTaskTool;
use App\Mcp\Tools\SearchTasksTool;
use Laravel\Mcp\Server;
class TaskServer extends Server
{
protected array $tools = [
CreateTaskTool::class,
CompleteTaskTool::class,
SearchTasksTool::class,
];
protected array $resources = [
TaskStatsResource::class,
RecentCompletedTasksResource::class,
];
protected array $prompts = [
ProductivityReportPrompt::class,
];
}
提示虽然微妙,但却十分有效。它们能让你编码关于人工智能如何分析数据的领域知识。
建造者模式的回报
在继续之前,我们先来欣赏一下自定义构建器所实现的功能。看看我们的代码是如何演变的:
之前(冗长且容易出错):
$tasks = Task::where('user_id', $userId)
->where('completed', false)
->where('priority', 'high')
->where('created_at', '>=', now()->subDays(7))
->get();
之后(富有表现力和可维护性):
$tasks = Task::forUser($userId)
->incomplete()
->highPriority()
->createdInPeriod(7)
->get();
发现区别了吗?第二个版本读起来像英文版。更重要的是:
- 它是可测试的
——我们可以单独测试每个构建器方法 - 可重复使用
- 逻辑集中在一个地方 - 它是可维护的
- 需要更改“高优先级”的工作方式吗?一个位置。 - 可组合
——混合搭配方法,自然构建复杂查询
未来的你会感谢现在的你写下这样的代码。相信我。
第 7 部分:添加身份验证
现在,任何找到您的 MCP 端点的人都可以创建和管理任务。让我们使用 Laravel Sanctum 来解决这个问题。
安装 Sanctum
如果您尚未安装 Sanctum:
php artisan install:api
这将设置 Sanctum 并创建必要的迁移。
保护您的 MCP 服务器
更新您的routes/ai.php:
<?php
use App\Mcp\Servers\TaskServer;
use Laravel\Mcp\Facades\Mcp;
Mcp::web('/mcp/tasks', TaskServer::class)
->middleware(['auth:sanctum']);
就是这样。现在,所有发送到 MCP 服务器的请求都需要一个有效的 Sanctum 令牌。
创建 API 令牌
让我们创建一个简单的命令来生成用于测试的令牌:
php artisan make:command CreateMcpToken
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class CreateMcpToken extends Command
{
protected $signature = 'mcp:token {email}';
protected $description = 'Create an MCP API token for a user';
public function handle(): void
{
$user = User::where('email', $this->argument('email'))->first();
if (!$user) {
$this->error('User not found');
return;
}
$token = $user->createToken('mcp-access')->plainTextToken;
$this->info('Token created successfully:');
$this->line($token);
$this->newLine();
$this->info('Use this in your Authorization header:');
$this->line("Bearer {$token}");
}
}
创建用户并生成令牌:
php artisan tinker
>>> User::create(['name' => 'Test User', 'email' => 'test@example.com', 'password' => bcrypt('password')]);
>>> exit
php artisan mcp:token test@example.com
现在您可以在 MCP 客户端配置中使用该令牌。
用户范围的任务
想要让任务特定于用户吗?在任务表中添加一个 user_id:
php artisan make:migration add_user_id_to_tasks_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
});
}
public function down(): void
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropColumn('user_id');
});
}
};
运行迁移:
php artisan migrate
更新您的任务模型:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Task extends Model
{
protected $fillable = [
'user_id',
'title',
'description',
'priority',
'completed',
'completed_at',
];
protected $casts = [
'completed' => 'boolean',
'completed_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function scopeForUser($query, $userId)
{
return $query->where('user_id', $userId);
}
public function scopeIncomplete($query)
{
return $query->where('completed', false);
}
public function scopeCompleted($query)
{
return $query->where('completed', true);
}
public function markComplete(): void
{
$this->update([
'completed' => true,
'completed_at' => now(),
]);
}
}
现在更新您的工具,使其范围限定为经过身份验证的用户。例如,在CreateTaskTool:
public function handle(Request $request): Response
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'priority' => ['sometimes', 'in:low,medium,high'],
], [
'title.required' => 'You must provide a task title. For example: "Review Q4 report" or "Call John about project".',
'title.max' => 'Task title is too long. Please keep it under 255 characters.',
'description.max' => 'Task description is too long. Please keep it under 1000 characters.',
'priority.in' => 'Priority must be one of: low, medium, or high.',
]);
// Add the authenticated user's ID
$validated['user_id'] = $request->user()->id;
$task = Task::create($validated);
return Response::text(
"✅ Task created successfully!\n\n" .
"**{$task->title}**\n" .
($task->description ? "{$task->description}\n" : '') .
"Priority: {$task->priority}\n" .
"ID: {$task->id}"
);
}
对其他工具和资源执行相同操作 - 始终使用范围查询Task::forUser($request->user()->id)。
第 8 部分:测试您的服务器
让我们为我们所构建的一切编写全面的测试。
更新你的测试
tests/Feature/TaskMcpTest.php使用完整测试套件进行更新:
<?php
use App\Mcp\Servers\TaskServer;
use App\Mcp\Tools\CompleteTaskTool;
use App\Mcp\Tools\CreateTaskTool;
use App\Mcp\Tools\SearchTasksTool;
use App\Models\Task;
use App\Models\User;
uses()->group('mcp');
beforeEach(function () {
$this->user = User::factory()->create();
$this->otherUser = User::factory()->create();
});
test('can create a task', function () {
$response = TaskServer::actingAs($this->user)->tool(CreateTaskTool::class, [
'title' => 'Write tutorial',
'description' => 'Complete the Laravel MCP tutorial',
'priority' => 'high',
]);
$response->assertOk();
$response->assertSee('Write tutorial');
$response->assertSee('high');
expect(Task::first())
->user_id->toBe($this->user->id)
->title->toBe('Write tutorial')
->priority->toBe('high')
->completed->toBeFalse();
});
test('can complete a task', function () {
$task = Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Test task',
'completed' => false,
]);
$response = TaskServer::actingAs($this->user)->tool(CompleteTaskTool::class, [
'task_id' => $task->id,
]);
$response->assertOk();
$response->assertSee('completed');
expect($task->fresh())
->completed->toBeTrue()
->completed_at->not->toBeNull();
});
test('can search tasks by priority', function () {
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Important meeting',
'priority' => 'high',
]);
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Review documents',
'priority' => 'low',
]);
$response = TaskServer::actingAs($this->user)->tool(SearchTasksTool::class, [
'priority' => 'high',
]);
$response->assertOk();
$response->assertSee('Important meeting');
$response->assertDontSee('Review documents');
});
test('can search by keyword', function () {
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Budget review meeting',
]);
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Team standup',
]);
$response = TaskServer::actingAs($this->user)->tool(SearchTasksTool::class, [
'keyword' => 'budget',
]);
$response->assertOk();
$response->assertSee('Budget review');
$response->assertDontSee('Team standup');
});
test('users only see their own tasks', function () {
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'My task',
]);
Task::factory()->create([
'user_id' => $this->otherUser->id,
'title' => 'Other user task',
]);
$response = TaskServer::actingAs($this->user)->tool(SearchTasksTool::class, []);
$response->assertOk();
$response->assertSee('My task');
$response->assertDontSee('Other user task');
});
test('task title is required', function () {
$response = TaskServer::actingAs($this->user)->tool(CreateTaskTool::class, [
'description' => 'A task without a title',
]);
$response->assertHasErrors();
});
test('invalid priority is rejected', function () {
$response = TaskServer::actingAs($this->user)->tool(CreateTaskTool::class, [
'title' => 'Test task',
'priority' => 'super-urgent',
]);
$response->assertHasErrors();
});
test('completing already completed task gives helpful message', function () {
$task = Task::factory()->completed()->create([
'user_id' => $this->user->id,
]);
$response = TaskServer::actingAs($this->user)->tool(CompleteTaskTool::class, [
'task_id' => $task->id,
]);
$response->assertOk();
$response->assertSee('already completed');
});
直接测试构建器
您还可以单独测试自定义构建器方法。这种做法看似矫枉过正,但实际生产环境之前,您很可能已经发现了 bug。创建tests/Unit/TaskBuilderTest.php:
<?php
use App\Models\Task;
use App\Models\User;
uses()->group('builder');
beforeEach(function () {
$this->user = User::factory()->create();
});
test('for user filters by user', function () {
$myTask = Task::factory()->create(['user_id' => $this->user->id]);
$otherTask = Task::factory()->create();
$tasks = Task::forUser($this->user->id)->get();
expect($tasks)->toContain($myTask)
->not->toContain($otherTask);
});
test('incomplete filters only incomplete tasks', function () {
$incomplete = Task::factory()->create([
'user_id' => $this->user->id,
'completed' => false,
]);
$complete = Task::factory()->completed()->create([
'user_id' => $this->user->id,
]);
$tasks = Task::forUser($this->user->id)->incomplete()->get();
expect($tasks)->toContain($incomplete)
->not->toContain($complete);
});
test('search finds tasks by title', function () {
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Budget review',
]);
Task::factory()->create([
'user_id' => $this->user->id,
'title' => 'Team meeting',
]);
$tasks = Task::forUser($this->user->id)->search('budget')->get();
expect($tasks)->toHaveCount(1)
->first()->title->toBe('Budget review');
});
test('high priority filters correctly', function () {
$highPriority = Task::factory()->create([
'user_id' => $this->user->id,
'priority' => 'high',
]);
Task::factory()->create([
'user_id' => $this->user->id,
'priority' => 'low',
]);
$tasks = Task::forUser($this->user->id)->highPriority()->get();
expect($tasks)->toHaveCount(1)
->toContain($highPriority);
});
test('builder methods are chainable', function () {
Task::factory()->create([
'user_id' => $this->user->id,
'priority' => 'high',
'completed' => false,
'title' => 'Important task',
]);
Task::factory()->create([
'user_id' => $this->user->id,
'priority' => 'high',
'completed' => true,
'title' => 'Completed important task',
]);
Task::factory()->create([
'user_id' => $this->user->id,
'priority' => 'low',
'completed' => false,
'title' => 'Low priority task',
]);
// Chain multiple builder methods - this is the magic
$tasks = Task::forUser($this->user->id)
->incomplete()
->highPriority()
->search('important')
->get();
expect($tasks)->toHaveCount(1)
->first()->title->toBe('Important task');
});
test('search handles null gracefully', function () {
Task::factory()->count(3)->create(['user_id' => $this->user->id]);
$tasks = Task::forUser($this->user->id)->search(null)->get();
expect($tasks)->toHaveCount(3);
});
test('recently completed filters by timeframe', function () {
// Task completed 5 days ago
$recentTask = Task::factory()->create([
'user_id' => $this->user->id,
'completed' => true,
'completed_at' => now()->subDays(5),
]);
// Task completed 40 days ago
Task::factory()->create([
'user_id' => $this->user->id,
'completed' => true,
'completed_at' => now()->subDays(40),
]);
$tasks = Task::forUser($this->user->id)->recentlyCompleted(30)->get();
expect($tasks)->toHaveCount(1)
->toContain($recentTask);
});
我喜欢这些构建器测试的原因如下:它们速度快(无需 HTTP 层)、专注(一次只关注一件事),并且能够记录构建器方法的实际工作方式。六个月后,当有人加入你的团队时,这些测试就变成了动态文档。
php artisan test
你应该会看到一些漂亮的内容——全是绿色的勾号。这就是代码经过良好测试的感觉。
不要忘记在以下位置创建任务工厂database/factories/TaskFactory.php:
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class TaskFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->sentence(),
'description' => fake()->paragraph(),
'priority' => fake()->randomElement(['low', 'medium', 'high']),
'completed' => false,
'completed_at' => null,
];
}
public function completed(): static
{
return $this->state(fn (array $attributes) => [
'completed' => true,
'completed_at' => fake()->dateTimeBetween('-30 days', 'now'),
]);
}
}
运行测试:
php artisan test
使用 MCP 检查器
Laravel MCP 包含一个用于交互式测试的内置检查器:
php artisan mcp:inspector /mcp/tasks
这将启动一个交互式工具,您可以在其中:
-
查看所有可用的工具、资源和提示 -
具有不同输入的测试工具 -
查看资源数据 -
尝试提示
它在开发过程中非常有价值。
第 9 部分:使用 MCP 服务器
现在到了有趣的部分——实际使用我们所构建的东西!
从 Claude Desktop 连接
-
启动你的 Laravel 服务器:
php artisan serve
-
打开您的 Claude Desktop 设置(或任何 MCP 客户端) -
添加您的服务器配置:
{
"mcpServers": {
"tasks": {
"url": "http://localhost:8000/mcp/tasks",
"headers": {
"Authorization": "Bearer YOUR_TOKEN_HERE"
}
}
}
}
-
重启Claude桌面
交互示例
连接后,您可以进行如下对话:
您:“创建一个高优先级的第四季度预算报告审核任务”
Claude:[调用 CreateTaskTool]✅ 任务创建成功!
审核第四季度预算报告优先级:高 ID:1
您:“我还有哪些待处理的任务?”
Claude:[调用 SearchTasksTool,状态为“未完成”]找到了 3 个任务:
⏳[1]审查第四季度预算报告 优先级:高
⏳[2]致电供应商了解交货情况 优先级:中
⏳[3]更新项目文档 优先级:低
您:“我审核完了预算。标记该任务为完成。”
Claude:[调用 CompleteTaskTool,task_id=1]✅ 任务完成!
审核第四季度预算报告完成时间:2025年10月7日下午2:30
你:“这周我的工作效率如何?”
Claude:[调用 ProductivityReportPrompt]根据你过去 7 天的任务数据:
您已完成 5 项任务,同时创建了 8 项新任务。一切尽在掌控!您还有 3 项未完成的任务,以及 1 项高优先级事项待处理。预算审核的完成时机恰到好处——像这样的高优先级事项确实会影响您一周的工作。
重点建议:趁预算细节还记忆犹新时,接下来处理供应商电话。
总结:您构建了什么
让我们回顾一下,欣赏一下我们在这里创建的内容。现在,您已经拥有一个功能齐全的 MCP 服务器,它:
-
✅公开三个用于创建、完成和搜索任务的工具 -
✅提供统计概览和近期活动两种资源 -
✅包含生产力分析提示 -
✅使用 Laravel Sanctum对用户进行身份验证 -
✅限制每个用户的数据,以确保隐私和安全 -
✅经过全面的可靠性测试 -
✅始终遵循 Laravel 最佳实践
但更重要的是,你要理解 MCP 的思维模式。这不仅仅是构建 API,而是构建 AI 能够真正理解并有效使用的接口。
下一步是什么?
扩展此内容的一些想法:
添加更多工具:
-
UpdateTaskTool 用于编辑现有任务 -
DeleteTaskTool 并确认 -
用于截止日期管理的 SetTaskDueDateTool -
BulkCompleteTasks批量操作工具
增强资源:
-
即将到来的截止日期资源 -
TasksByProjectResource(如果您添加项目) -
TimeTrackingResource(如果你跟踪花费的时间)
高级功能:
-
任务标签和类别 -
子任务和依赖关系 -
任务到期时的通知 -
与外部服务集成(Google 日历等) -
自然语言截止日期解析
更好的提示:
-
DailyPlanningPrompt 用于早晨任务规划 -
每周回顾提示 -
FocusTimePrompt 建议深度工作会议
资源
- Laravel MCP 文档
- 模型上下文协议规范
- Laravel 圣殿
- 本教程的完整源代码(占位符)
最后的想法
我第一次探索 MCP 时,就被它的自然体验所震撼。几十年来,我们一直在构建 API,但这次不同。我们不仅仅是提供数据和功能,我们还在构建 AI 真正能够用来帮助人类的工具。
我们今天构建的任务管理器很简单,但它展示了一些有趣的东西:你的 Laravel 应用现在可以实现 AI 原生化了。它们可以智能地响应自然语言请求,提供上下文感知信息,并以对话的方式帮助用户实现目标。
关注【索引目录】服务号,更多精彩内容等你来探索!

