什麼是 MCP 伺服器(進階版)
Model Context Protocol(MCP) 是讓 AI 客戶端(如 Claude、Cursor、GitHub Copilot 等)與應用透過標準化協議進行通訊的規範。MCP 有 3 類主要原語。| 原語 | 職責 | 典型用途 |
|---|---|---|
| 工具(Tools) | AI 可呼叫的函式 | 資料操作、外部 API 整合、計算 |
| 資源(Resources) | AI 可讀取的上下文 | 文件、配置資訊、動態資料 |
| 提示(Prompts) | 可複用的模板 | 常用查詢、工作流引導 |
本進階指南聚焦於實戰實現。有關 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
註冊伺服器
在 Web 伺服器透過 HTTP POST 訪問。本地伺服器則作為 Artisan 命令執行,用於與 CLI 型 AI 客戶端整合。
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);
Tool 的實現
Tool 是 AI 客戶端可呼叫的函式。可以直接使用 Laravel 的服務容器、驗證、Eloquent。建立 Tool
php artisan make:mcp-tool SearchUsersTool
handle 與 schema 方法。
引數定義(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}");
}
}
條件註冊
可以僅向滿足特定條件的使用者暴露工具。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));
}
}
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');
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 伺服器的效能與異常。