大数跨境

使用 Laravel 构建您的第一个 MCP 服务器

使用 Laravel 构建您的第一个 MCP 服务器 索引目录
2025-10-10
2
导读:关注【索引目录】服务号,更多精彩内容等你来探索!简介:MCP 为何改变一切说实话,当我第一次听说模型上下文协议 (MCP) 时,我心存疑虑。

关注【索引目录】服务号,更多精彩内容等你来探索!

简介: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 连接

  1. 启动你的 Laravel 服务器:
php artisan serve
  1. 打开您的 Claude Desktop 设置(或任何 MCP 客户端)
  2. 添加您的服务器配置:
{
  "mcpServers": {
    "tasks": {
      "url": "http://localhost:8000/mcp/tasks",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN_HERE"
      }
    }
  }
}
  1. 重启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 原生化了。它们可以智能地响应自然语言请求,提供上下文感知信息,并以对话的方式帮助用户实现目标。

关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444