跳转到主要内容

什么是 MCP 服务器(进阶版)

Model Context Protocol(MCP) 是让 AI 客户端(如 Claude、Cursor、GitHub Copilot 等)与应用通过标准化协议进行通信的规范。MCP 有 3 类主要原语。
原语职责典型用途
工具(Tools)AI 可调用的函数数据操作、外部 API 集成、计算
资源(Resources)AI 可读取的上下文文档、配置信息、动态数据
提示(Prompts)可复用的模板常用查询、工作流引导
用 Laravel 构建 MCP 服务器的好处在于可以直接使用 Eloquent、缓存、认证、验证等 Laravel 生态。
本进阶指南聚焦于实战实现。有关 MCP 的基础概念请参考 中级:Laravel MCP

安装与初始化

1

安装包

使用 Composer 安装包。
composer require laravel/mcp
2

发布路由文件

使用 vendor:publish 生成用于注册 MCP 服务器的 routes/ai.php
php artisan vendor:publish --tag=ai-routes
3

生成服务器类

通过 Artisan 命令创建服务器类。
php artisan make:mcp-server DatabaseServer
在生成的 app/Mcp/Servers/DatabaseServer.php 中注册 Tool、Resource、Prompt。
4

注册服务器

routes/ai.php 中将服务器注册到路由。
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('/mcp/database', DatabaseServer::class);
use App\Mcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::local('database', DatabaseServer::class);
Web 服务器通过 HTTP POST 访问。本地服务器则作为 Artisan 命令运行,用于与 CLI 型 AI 客户端集成。

Tool 的实现

Tool 是 AI 客户端可调用的函数。可以直接使用 Laravel 的服务容器、验证、Eloquent。

创建 Tool

php artisan make:mcp-tool SearchUsersTool
在生成的类中实现 handleschema 方法。

参数定义(Schema)

schema 方法中使用 Illuminate\Contracts\JsonSchema\JsonSchema builder 定义接受的参数。
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('按姓名或邮箱搜索用户。')]
class SearchUsersTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'query'   => 'required|string|max:100',
            'limit'   => 'integer|min:1|max:50',
            'role'    => 'nullable|string|in:admin,editor,viewer',
        ], [
            'query.required' => '请指定搜索关键词,例如 "张三" 或 "[email protected]"',
            'limit.max'      => '获取条数最多为 50 条。',
            'role.in'        => '角色必须为 admin、editor、viewer 之一。',
        ]);

        $users = \App\Models\User::query()
            ->where(function ($q) use ($validated) {
                $q->where('name', 'like', "%{$validated['query']}%")
                  ->orWhere('email', 'like', "%{$validated['query']}%");
            })
            ->when(isset($validated['role']), fn ($q) => $q->where('role', $validated['role']))
            ->limit($validated['limit'] ?? 10)
            ->get(['id', 'name', 'email', 'role']);

        if ($users->isEmpty()) {
            return Response::text('未找到符合条件的用户。');
        }

        $result = $users->map(fn ($u) => "ID:{$u->id} {$u->name} <{$u->email}> [{$u->role}]")
                        ->implode("\n");

        return Response::text($result);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'query' => $schema->string()
                ->description('搜索关键词。按姓名或邮箱搜索。')
                ->required(),

            'limit' => $schema->integer()
                ->description('返回条数上限(默认 10,最大 50)。')
                ->minimum(1)
                ->maximum(50)
                ->default(10),

            'role' => $schema->string()
                ->description('要筛选的角色。admin、editor 或 viewer。')
                ->enum(['admin', 'editor', 'viewer']),
        ];
    }
}

Tool 注解

使用 MCP 协议提供的注解可以让 AI 客户端判断工具的安全性。
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;

#[IsReadOnly]      // 不改变环境的只读工具
#[IsIdempotent]    // 相同参数多次调用结果一致
class SearchUsersTool extends Tool
{
    // ...
}
注解说明
#[IsReadOnly]不修改数据
#[IsDestructive]执行删除等破坏性操作
#[IsIdempotent]相同参数重复执行是安全的
#[IsOpenWorld]与外部实体通信

结构化响应

若希望返回易于 AI 客户端解析的 JSON 响应,使用 Response::structured
return Response::structured([
    'total' => $users->count(),
    'users' => $users->map(fn ($u) => [
        'id'    => $u->id,
        'name'  => $u->name,
        'email' => $u->email,
    ])->toArray(),
]);

流式响应

对耗时较长的处理,返回 Generator 可以实现进度流式推送。
use Generator;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;

public function handle(Request $request): Generator
{
    $items = \App\Models\Product::all();
    $total = $items->count();

    foreach ($items as $index => $item) {
        yield Response::notification('processing/progress', [
            'current' => $index + 1,
            'total'   => $total,
            'name'    => $item->name,
        ]);

        // 重处理...
        yield Response::text("已完成处理:{$item->name}");
    }
}
在 Web 服务器上,流式响应会自动作为 SSE(Server-Sent Events)流发送。

条件注册

可以仅向满足特定条件的用户暴露工具。
public function shouldRegister(Request $request): bool
{
    return $request?->user()?->hasRole('admin') ?? false;
}

Resource 的实现

Resource 是 AI 客户端作为上下文加载的数据,如文档、配置、动态数据。

静态资源

php artisan make:mcp-resource AppDocumentationResource
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Attributes\Uri;
use Laravel\Mcp\Server\Resource;

#[Uri('app://resources/documentation')]
#[MimeType('text/markdown')]
#[Description('应用的 API 文档与业务规则概述。')]
class AppDocumentationResource extends Resource
{
    public function handle(Request $request): Response
    {
        $content = \Illuminate\Support\Facades\Storage::get('docs/api-overview.md')
            ?? '未找到文档。';

        return Response::text($content);
    }
}

动态资源(URI 模板)

使用 URI 模板可以根据 URL 参数提供动态资源。
<?php

namespace App\Mcp\Resources;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Attributes\MimeType;
use Laravel\Mcp\Server\Contracts\HasUriTemplate;
use Laravel\Mcp\Server\Resource;
use Laravel\Mcp\Support\UriTemplate;

#[MimeType('application/json')]
#[Description('根据用户 ID 获取用户信息。')]
class UserProfileResource extends Resource implements HasUriTemplate
{
    public function uriTemplate(): UriTemplate
    {
        return new UriTemplate('app://users/{userId}/profile');
    }

    public function handle(Request $request): Response
    {
        $userId = $request->get('userId');

        $user = \App\Models\User::find($userId);

        if (! $user) {
            return Response::error("未找到用户 ID {$userId}。");
        }

        return Response::text(json_encode([
            'id'         => $user->id,
            'name'       => $user->name,
            'email'      => $user->email,
            'created_at' => $user->created_at->toIso8601String(),
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
    }
}
AI 客户端会请求 app://users/42/profile 这样的 URI,{userId} 的值可以通过 $request->get('userId') 获得。

Resource 注解

可以显式声明资源的优先级与受众。
use Laravel\Mcp\Enums\Role;
use Laravel\Mcp\Server\Annotations\Audience;
use Laravel\Mcp\Server\Annotations\Priority;

#[Audience(Role::Assistant)]   // 面向 AI 助手
#[Priority(0.8)]               // 重要程度(0.0〜1.0)
class AppDocumentationResource extends Resource
{
    // ...
}

Prompt 的实现

Prompt 是 AI 客户端可使用的可复用模板。用于将常用查询或复杂工作流标准化。

创建 Prompt

php artisan make:mcp-prompt DataAnalysisPrompt
<?php

namespace App\Mcp\Prompts;

use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Prompt;
use Laravel\Mcp\Server\Prompts\Argument;

#[Description('用于生成数据分析报告的 Prompt 模板。')]
class DataAnalysisPrompt extends Prompt
{
    public function arguments(): array
    {
        return [
            new Argument(
                name: 'target',
                description: '分析对象。例如:"上月销售"、"用户注册数变化"',
                required: true,
            ),
            new Argument(
                name: 'format',
                description: '输出格式。"summary"(摘要)或 "detail"(详细)',
                required: false,
            ),
        ];
    }

    public function handle(Request $request): array
    {
        $validated = $request->validate([
            'target' => 'required|string|max:200',
            'format' => 'nullable|in:summary,detail',
        ]);

        $target = $validated['target'];
        $format = $validated['format'] ?? 'summary';
        $instruction = $format === 'detail'
            ? '请生成包含数值、趋势、异常值与推荐操作的详细报告。'
            : '请生成包含关键指标与三条重要洞察的简洁摘要。';

        return [
            Response::text(
                "你是一名数据分析师。{$instruction}"
            )->asAssistant(),
            Response::text(
                "请分析以下数据:{$target}"
            ),
        ];
    }
}
使用 asAssistant() 后消息会被视作 AI 助手的发言。将系统提示与用户消息组合,可以细致控制 AI 的行为。

认证与授权

使用 Sanctum 的令牌认证

这是最简单的认证方式。MCP 客户端需带上 Authorization: Bearer <token> 头。
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:sanctum');

基于 OAuth 2.1 的认证

若需要更稳固的认证,使用 Laravel Passport。
// routes/ai.php
use Laravel\Mcp\Facades\Mcp;

Mcp::oauthRoutes();

Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware('auth:api');
采用 OAuth 认证时,请发布 MCP 提供的授权视图,并在 AppServiceProvider 中做设置。
php artisan vendor:publish --tag=mcp-views
// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;

public function boot(): void
{
    Passport::authorizationView(function ($parameters) {
        return view('mcp.authorize', $parameters);
    });
}

使用自定义中间件的认证

若使用自定义 API 令牌,可通过中间件校验 Authorization 头。
// app/Http/Middleware/McpTokenMiddleware.php
public function handle($request, Closure $next)
{
    $token = $request->bearerToken();

    if (! $token || ! \App\Models\ApiToken::where('token', hash('sha256', $token))->exists()) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    return $next($request);
}

Tool 内部的授权

在 Tool 或 Resource 的 handle 中可以通过 $request->user() 做细粒度授权检查。
public function handle(Request $request): Response
{
    $user = $request->user();

    if (! $user?->can('manage-users')) {
        return Response::error('你没有使用该工具的权限,请联系管理员。');
    }

    // 继续已授权的处理...
}
shouldRegister 只是将工具从列表中隐藏。调用工具时的授权检查必须在 handle 方法内进行。

实战示例:数据库操作工具

以下是使用 Eloquent 检索与创建数据的完整示例。

服务器类

<?php

namespace App\Mcp\Servers;

use App\Mcp\Tools\CreatePostTool;
use App\Mcp\Tools\SearchPostsTool;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;
use Laravel\Mcp\Server;

#[Name('Blog Management Server')]
#[Version('1.0.0')]
#[Instructions('可以搜索和创建博客文章的 MCP 服务器。')]
class BlogServer extends Server
{
    protected array $tools = [
        SearchPostsTool::class,
        CreatePostTool::class,
    ];
}

检索工具(只读)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsIdempotent;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('按关键词或状态搜索博客文章。')]
#[IsReadOnly]
#[IsIdempotent]
class SearchPostsTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'keyword' => 'nullable|string|max:100',
            'status'  => 'nullable|in:draft,published,archived',
            'limit'   => 'integer|min:1|max:20',
        ]);

        $posts = Post::query()
            ->when($validated['keyword'] ?? null, fn ($q, $kw) =>
                $q->where('title', 'like', "%{$kw}%")
                  ->orWhere('body', 'like', "%{$kw}%")
            )
            ->when($validated['status'] ?? null, fn ($q, $s) => $q->where('status', $s))
            ->latest()
            ->limit($validated['limit'] ?? 5)
            ->get(['id', 'title', 'status', 'published_at']);

        if ($posts->isEmpty()) {
            return Response::text('未找到符合条件的文章。');
        }

        return Response::structured([
            'total' => $posts->count(),
            'posts' => $posts->map(fn ($p) => [
                'id'           => $p->id,
                'title'        => $p->title,
                'status'       => $p->status,
                'published_at' => $p->published_at?->toIso8601String(),
            ])->toArray(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'keyword' => $schema->string()
                ->description('标题或正文中包含的关键词。'),

            'status' => $schema->string()
                ->description('文章状态。')
                ->enum(['draft', 'published', 'archived']),

            'limit' => $schema->integer()
                ->description('返回条数上限(默认 5,最大 20)。')
                ->default(5),
        ];
    }
}

创建工具(写入)

<?php

namespace App\Mcp\Tools;

use App\Models\Post;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('以草稿形式创建新的博客文章。')]
class CreatePostTool extends Tool
{
    public function handle(Request $request): Response
    {
        $user = $request->user();

        if (! $user?->can('create-posts')) {
            return Response::error('你没有创建文章的权限。');
        }

        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
            'tags'  => 'nullable|array',
            'tags.*' => 'string|max:50',
        ], [
            'title.required' => '请指定文章标题。',
            'body.required'  => '请指定文章正文。',
        ]);

        $post = Post::create([
            'title'   => $validated['title'],
            'body'    => $validated['body'],
            'status'  => 'draft',
            'user_id' => $user->id,
        ]);

        if (! empty($validated['tags'])) {
            $post->syncTags($validated['tags']);
        }

        return Response::structured([
            'id'         => $post->id,
            'title'      => $post->title,
            'status'     => $post->status,
            'created_at' => $post->created_at->toIso8601String(),
            'message'    => '文章已创建为草稿。',
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'title' => $schema->string()
                ->description('文章标题(最多 255 字符)。')
                ->required(),

            'body' => $schema->string()
                ->description('文章正文。支持 Markdown。')
                ->required(),

            'tags' => $schema->array()
                ->description('要打上的标签数组。例如:["Laravel", "PHP"]'),
        ];
    }
}

实战示例:文件系统操作工具

使用 Storage Facade 操作文件的示例。
<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Storage;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
use Laravel\Mcp\Server\Tool;

#[Description('获取存储中的文件列表,并读取文本文件内容。')]
#[IsReadOnly]
class ReadFileTool extends Tool
{
    public function handle(Request $request): Response
    {
        $validated = $request->validate([
            'path'    => 'required|string|max:500',
            'disk'    => 'nullable|string|in:local,public,s3',
        ]);

        $disk = $validated['disk'] ?? 'local';
        $path = $validated['path'];

        // 防止目录遍历攻击
        if (str_contains($path, '..')) {
            return Response::error('指定了无效路径。');
        }

        if (! Storage::disk($disk)->exists($path)) {
            return Response::error("未找到文件:{$path}");
        }

        $mimeType = Storage::disk($disk)->mimeType($path);

        // 仅返回文本文件内容
        if (str_starts_with($mimeType, 'text/') || $mimeType === 'application/json') {
            $content = Storage::disk($disk)->get($path);
            return Response::text($content);
        }

        // 图片文件以二进制返回
        if (str_starts_with($mimeType, 'image/')) {
            return Response::fromStorage($path, disk: $disk);
        }

        return Response::error("不支持的文件格式:{$mimeType}");
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'path' => $schema->string()
                ->description('要读取的文件路径。例如:"reports/2025-01.csv"')
                ->required(),

            'disk' => $schema->string()
                ->description('使用的存储磁盘。')
                ->enum(['local', 'public', 's3'])
                ->default('local'),
        ];
    }
}
文件操作工具必须对路径做规范化处理,避免访问允许目录以外的位置。含 .. 的路径应当拒绝。

测试

MCP 服务器、Tool、Resource、Prompt 都可以用 Laravel 标准测试机制编写单元测试。

测试 Tool

通过 Server::tool() 方法直接调用工具进行测试。
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

test('可以搜索文章', function () {
    Post::factory()->create(['title' => 'Laravel 基础', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP 高级', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Laravel 基础');
});

test('无权限用户不能创建文章', function () {
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => '测试文章',
            'body'  => '正文',
        ]);

    $response->assertSee('你没有创建文章的权限');
});
use App\Mcp\Servers\BlogServer;
use App\Mcp\Tools\SearchPostsTool;
use App\Models\Post;
use App\Models\User;

public function test_可以搜索文章(): void
{
    Post::factory()->create(['title' => 'Laravel 基础', 'status' => 'published']);
    Post::factory()->create(['title' => 'PHP 高级', 'status' => 'draft']);

    $response = BlogServer::tool(SearchPostsTool::class, [
        'keyword' => 'Laravel',
        'status'  => 'published',
    ]);

    $response
        ->assertOk()
        ->assertSee('Laravel 基础');
}

public function test_无权限用户不能创建文章(): void
{
    $user = User::factory()->create();

    $response = BlogServer::actingAs($user)
        ->tool(\App\Mcp\Tools\CreatePostTool::class, [
            'title' => '测试文章',
            'body'  => '正文',
        ]);

    $response->assertSee('你没有创建文章的权限');
}

Resource 与 Prompt 的测试

// Resource 测试
$response = BlogServer::resource(\App\Mcp\Resources\AppDocumentationResource::class);
$response->assertOk()->assertSee('API');

// Prompt 测试
$response = BlogServer::prompt(\App\Mcp\Prompts\DataAnalysisPrompt::class, [
    'target' => '上月销售',
    'format' => 'summary',
]);
$response->assertOk()->assertSee('数据分析师');

主要断言方法

方法说明
assertOk()断言响应无错误
assertSee($text)断言响应中包含指定文本
assertHasErrors()断言响应存在错误
assertHasNoErrors()断言响应无错误
assertName($name)断言 Tool 名
assertSentNotification($method, $data)断言已发送通知
assertNotificationCount($count)断言通知发送次数

使用 MCP Inspector 调试

对于交互式调试,使用 MCP Inspector。
# 检查 Web 服务器
php artisan mcp:inspector mcp/database

# 检查本地服务器
php artisan mcp:inspector database

部署要点

HTTP 流式与 SSE

在 Web 服务器上使用流式响应(Generator)时,请检查 Web 服务器配置。
location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_buffering off;          # SSE 需要关闭缓冲
    proxy_cache off;
    proxy_read_timeout 3600s;     # 支持长连接
    proxy_set_header Connection '';
    chunked_transfer_encoding on;
}
# 使用 mod_proxy_http
ProxyPass /mcp http://127.0.0.1:8000/mcp
ProxyPassReverse /mcp http://127.0.0.1:8000/mcp
SetEnv proxy-sendchunks 1

与 Laravel Octane 组合

对高并发的 MCP 服务器,可考虑使用 Laravel Octane(FrankenPHP 或 Swoole),能显著减少每次请求的开销。
使用 Octane 时请求间会共享状态。请注意不要在 Tool 中使用静态属性或全局状态。

限流

throttle 中间件限制对 MCP 服务器的请求。
// 在 config/cache.php 中定义专用限流器
// routes/ai.php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'throttle:60,1']);

缓存

对频繁调用的只读 Tool 建议使用缓存。
public function handle(Request $request): Response
{
    $data = \Illuminate\Support\Facades\Cache::remember(
        "mcp:search:{$request->get('keyword')}",
        now()->addMinutes(5),
        fn () => $this->fetchFromDatabase($request)
    );

    return Response::structured($data);
}

日志与监控

对 MCP Tool 调用记录日志,可以掌握 AI 客户端的使用情况。
public function handle(Request $request): Response
{
    \Illuminate\Support\Facades\Log::info('MCP tool called', [
        'tool'   => static::class,
        'user'   => $request->user()?->id,
        'params' => $request->all(),
    ]);

    // ...
}
生产环境推荐使用 Laravel Telescope 或 Sentry 监控 MCP 服务器的性能与异常。
最后修改于 2026年7月13日