Model Context Protocol (MCP) trong PHP: Kết Nối AI với Ứng Dụng
AI models mạnh nhưng cô lập. Chúng không thể truy cập database, gọi API, hay đọc files — trừ khi bạn build tích hợp cho từng model provider. Model Context Protocol (MCP) chuẩn hóa điều này. Build một MCP server, và bất kỳ MCP-compatible AI client nào (Claude, Cursor, VS Code Copilot) đều dùng được tools của bạn.
MCP giải quyết bài toán N×M integrations bằng cách tạo một giao diện tiêu chuẩn giữa AI clients và tools/data sources.
MCP Là Gì?
Trước MCP:
Claude ←→ Custom Claude integration
ChatGPT ←→ Custom OpenAI plugin
Cursor ←→ Custom Cursor extension
(N clients × M tools = N×M integrations cần build)
Sau MCP:
Claude ←→ MCP Client ←→ MCP Server ←→ Tools/Data của bạn
ChatGPT ←→ MCP Client ←↗
Cursor ←→ MCP Client ←↗
VS Code ←→ MCP Client ←↗
(N clients × 1 MCP server = N integrations, build 1 lần)
Analogy: MCP giống USB — trước khi có USB, mỗi thiết bị cần connector riêng. USB chuẩn hóa kết nối. MCP làm điều tương tự cho AI tools.
MCP Concepts
| Concept | Mô tả | Ví dụ |
|---|---|---|
| Server | Ứng dụng expose capabilities cho AI | Laravel app expose search, CRUD operations |
| Client | AI app kết nối MCP servers | Claude Desktop, Cursor, VS Code Copilot |
| Tools | Functions AI có thể gọi | search_posts, create_draft, deploy_app |
| Resources | Data AI có thể đọc | Database records, files, configs |
| Prompts | Templates AI có thể dùng | Code review template, summarize template |
Tools vs Resources
Tools = actions (đếng từ) → "Tìm bài viết", "Tạo draft", "Gửi email"
Resources = data (danh từ) → "Danh sách posts", "Nội dung file config", "User profile"
Tools thay đổi state hoặc thực hiện computation. Resources cung cấp data read-only.
Transport Types
MCP hỗ trợ hai cách giao tiếp:
| Transport | Cách hoạt động | Dùng khi |
|---|---|---|
| stdio | Client spawn process, communicate qua stdin/stdout | Local tools, CLI apps |
| HTTP SSE | Client kết nối HTTP, server stream responses | Remote servers, web services |
stdio phổ biến nhất cho local development — client chạy php artisan mcp:serve và communicate qua stdin/stdout.
Xây MCP Server Bằng PHP
Cài đặt
composer require modelcontextprotocol/php-sdk
MCP Server Command
// app/Console/Commands/McpServerCommand.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use ModelContextProtocol\Server\McpServer;
use ModelContextProtocol\Server\ServerCapabilities;
use App\Services\MarkdownPostService;
class McpServerCommand extends Command
{
protected $signature = 'mcp:serve';
protected $description = 'Khởi động MCP server cho AI assistants';
public function handle(MarkdownPostService $postService): int
{
$server = new McpServer(
name: 'laravel-blog-mcp',
version: '1.0.0',
capabilities: new ServerCapabilities(
tools: true,
resources: true,
prompts: true,
),
);
$this->registerTools($server, $postService);
$this->registerResources($server, $postService);
$this->registerPrompts($server);
// Chạy stdio transport — communicate qua stdin/stdout
$server->run();
return Command::SUCCESS;
}
}
Đăng Ký Tools
private function registerTools(McpServer $server, MarkdownPostService $postService): void
{
// Tool 1: Tìm kiếm bài viết
$server->registerTool(
name: 'search_posts',
description: 'Tìm kiếm blog posts theo từ khóa. Hỗ trợ tiếng Việt và tiếng Anh.',
inputSchema: [
'type' => 'object',
'properties' => [
'query' => [
'type' => 'string',
'description' => 'Từ khóa tìm kiếm',
],
'locale' => [
'type' => 'string',
'enum' => ['vi', 'en'],
'description' => 'Ngôn ngữ (mặc định: vi)',
],
'limit' => [
'type' => 'integer',
'description' => 'Số kết quả tối đa (mặc định: 5, tối đa: 20)',
],
],
'required' => ['query'],
],
handler: function (array $args) use ($postService): array {
$locale = $args['locale'] ?? 'vi';
$limit = min($args['limit'] ?? 5, 20);
$posts = $postService->search($args['query'], $locale, $limit);
return [
'content' => [
[
'type' => 'text',
'text' => json_encode($posts->map(fn ($p) => [
'title' => $p['title'],
'slug' => $p['slug'],
'description' => $p['description'],
'date' => $p['date'],
'tags' => $p['tags'],
])->toArray(), JSON_UNESCAPED_UNICODE),
],
],
];
},
);
// Tool 2: Lấy nội dung đầy đủ bài viết
$server->registerTool(
name: 'get_post_content',
description: 'Lấy nội dung markdown đầy đủ của bài viết theo slug. Dùng sau search_posts để đọc chi tiết.',
inputSchema: [
'type' => 'object',
'properties' => [
'slug' => [
'type' => 'string',
'description' => 'Post slug (lấy từ kết quả search_posts)',
],
'locale' => [
'type' => 'string',
'enum' => ['vi', 'en'],
'description' => 'Ngôn ngữ (mặc định: vi)',
],
],
'required' => ['slug'],
],
handler: function (array $args) use ($postService): array {
$post = $postService->getPost($args['slug'], $args['locale'] ?? 'vi');
if (!$post) {
return [
'content' => [['type' => 'text', 'text' => 'Không tìm thấy bài viết']],
'isError' => true,
];
}
return [
'content' => [
['type' => 'text', 'text' => "# {$post['title']}\n\n{$post['raw_content']}"],
],
];
},
);
// Tool 3: Tạo draft bài viết mới
$server->registerTool(
name: 'create_draft',
description: 'Tạo draft bài viết mới dưới dạng markdown file. Bài viết sẽ có draft: true.',
inputSchema: [
'type' => 'object',
'properties' => [
'title' => ['type' => 'string', 'description' => 'Tiêu đề bài viết'],
'slug' => ['type' => 'string', 'description' => 'URL slug'],
'description' => ['type' => 'string', 'description' => 'Mô tả ngắn'],
'tags' => [
'type' => 'array',
'items' => ['type' => 'string'],
'description' => 'Danh sách tags',
],
'content' => ['type' => 'string', 'description' => 'Nội dung markdown'],
'locale' => ['type' => 'string', 'enum' => ['vi', 'en']],
],
'required' => ['title', 'slug', 'content'],
],
handler: function (array $args): array {
$locale = $args['locale'] ?? 'vi';
$date = now()->format('Y-m-d');
$day = now()->format('d');
$frontmatter = "---\n";
$frontmatter .= "title: \"{$args['title']}\"\n";
$frontmatter .= "date: {$date}\n";
$frontmatter .= "description: \"{$args['description'] ?? ''}\"\n";
$frontmatter .= "tags: " . json_encode($args['tags'] ?? []) . "\n";
$frontmatter .= "draft: true\n";
$frontmatter .= "---\n\n";
$content = $frontmatter . $args['content'];
$year = now()->format('Y');
$month = now()->format('m');
$filePath = content_path("posts/{$locale}/{$year}/{$month}/{$day}-{$args['slug']}.md");
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
return [
'content' => [
['type' => 'text', 'text' => "Draft tạo thành công tại: {$filePath}"],
],
];
},
);
}
Đăng Ký Resources
private function registerResources(McpServer $server, MarkdownPostService $postService): void
{
// Resource: Danh sách tất cả bài viết
$server->registerResource(
uri: 'blog://posts/list',
name: 'Danh sách bài viết',
description: 'Tất cả bài viết đã publish trên blog',
mimeType: 'application/json',
handler: function () use ($postService): array {
$posts = $postService->getAllPosts('vi');
return [
'content' => [
[
'type' => 'text',
'text' => json_encode($posts->map(fn ($p) => [
'title' => $p['title'],
'slug' => $p['slug'],
'date' => $p['date'],
'tags' => $p['tags'],
]), JSON_UNESCAPED_UNICODE),
],
],
];
},
);
// Resource: Blog config/stats
$server->registerResource(
uri: 'blog://stats',
name: 'Blog Statistics',
description: 'Thống kê blog: số bài, tags phổ biến, bài mới nhất',
mimeType: 'application/json',
handler: function () use ($postService): array {
$posts = $postService->getAllPosts('vi');
$stats = [
'total_posts' => $posts->count(),
'languages' => ['vi', 'en'],
'popular_tags' => $posts->flatMap(fn ($p) => $p['tags'])->countBy()->sortDesc()->take(10),
'latest_post' => $posts->first()['title'] ?? null,
];
return [
'content' => [['type' => 'text', 'text' => json_encode($stats, JSON_UNESCAPED_UNICODE)]],
];
},
);
}
Đăng Ký Prompts
private function registerPrompts(McpServer $server): void
{
$server->registerPrompt(
name: 'review_post',
description: 'Review bài viết blog: kiểm tra chất lượng, SEO, và gợi ý cải thiện',
arguments: [
['name' => 'slug', 'description' => 'Slug của bài viết cần review', 'required' => true],
],
handler: function (array $args): array {
return [
'messages' => [
[
'role' => 'user',
'content' => [
[
'type' => 'text',
'text' => "Hãy review bài viết có slug '{$args['slug']}'. " .
"Kiểm tra:\n" .
"1. Nội dung có chính xác kỹ thuật không?\n" .
"2. Code examples có đúng và chạy được không?\n" .
"3. SEO: title, description, headings có tốt không?\n" .
"4. Cấu trúc bài viết có logic không?\n" .
"5. Có thiếu sections quan trọng nào không?\n\n" .
"Đầu tiên, dùng tool get_post_content để đọc bài viết.",
],
],
],
],
];
},
);
}
Cấu Hình AI Clients
Claude Desktop
// ~/.config/Claude/claude_desktop_config.json (Linux)
// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
{
"mcpServers": {
"my-blog": {
"command": "php",
"args": ["/path/to/project/artisan", "mcp:serve"],
"env": {
"APP_ENV": "local"
}
}
}
}
Sau khi save, restart Claude Desktop. Bạn sẽ thấy tools icon — Claude có thể search posts, đọc nội dung, tạo drafts.
VS Code (Copilot)
// .vscode/mcp.json (trong project root)
{
"servers": {
"blog": {
"type": "stdio",
"command": "php",
"args": ["artisan", "mcp:serve"]
}
}
}
Cursor
// ~/.cursor/mcp.json
{
"mcpServers": {
"my-blog": {
"command": "php",
"args": ["/path/to/project/artisan", "mcp:serve"]
}
}
}
Error Handling
$server->registerTool(
name: 'risky_operation',
// ...
handler: function (array $args): array {
try {
$result = $this->performOperation($args);
return [
'content' => [['type' => 'text', 'text' => json_encode($result)]],
];
} catch (ValidationException $e) {
return [
'content' => [['type' => 'text', 'text' => "Validation error: {$e->getMessage()}"]],
'isError' => true,
];
} catch (\Throwable $e) {
Log::error('MCP tool error', ['tool' => 'risky_operation', 'error' => $e->getMessage()]);
return [
'content' => [['type' => 'text', 'text' => 'Có lỗi xảy ra khi thực hiện thao tác.']],
'isError' => true,
];
}
},
);
isError: true nói với AI client: "tool call thất bại" → AI sẽ xử lý khác (thử cách khác, báo user) thay vì treat error message như data thành công.
Bảo Mật
MCP server chạy với quyền của user khởi động nó. Security checklist:
1. Authentication: Server chỉ chạy local (stdio) hoặc cần auth token (HTTP SSE)
2. Authorization: Check user permissions trước khi thực thi tools
3. Input validation: LUÔN validate/sanitize tool arguments
- SQL injection: dùng Eloquent/prepared statements
- Path traversal: validate file paths
- Command injection: KHÔNG bao giờ exec() tool arguments
4. Rate limiting: Prevent AI gọi tool quá nhiều
5. Read-only first: Bắt đầu chỉ với read tools, thêm write sau
6. Logging: Log tất cả tool executions cho audit
// Ví dụ: Validate path traversal
handler: function (array $args): array {
$slug = $args['slug'];
// ❌ NGUY HIỂM: slug có thể chứa "../../../etc/passwd"
// file_get_contents("content/posts/{$slug}.md");
// ✅ AN TOÀN: validate slug format
if (!preg_match('/^[a-z0-9\-]+$/', $slug)) {
return ['content' => [['type' => 'text', 'text' => 'Invalid slug format']], 'isError' => true];
}
$post = $postService->getPost($slug);
// ...
},
Testing MCP Server
class McpToolTest extends TestCase
{
public function test_search_posts_returns_matching_results(): void
{
// Tạo test content
$this->createTestPost('Laravel Caching Tips', 'laravel-caching-tips');
$handler = $this->getToolHandler('search_posts');
$result = $handler(['query' => 'caching']);
$text = json_decode($result['content'][0]['text'], true);
$this->assertNotEmpty($text);
$this->assertStringContainsString('Caching', $text[0]['title']);
}
public function test_get_post_returns_error_for_invalid_slug(): void
{
$handler = $this->getToolHandler('get_post_content');
$result = $handler(['slug' => 'nonexistent-post']);
$this->assertTrue($result['isError']);
}
public function test_create_draft_creates_file(): void
{
$handler = $this->getToolHandler('create_draft');
$result = $handler([
'title' => 'Test Post',
'slug' => 'test-post',
'content' => '# Hello World',
]);
$this->assertStringContainsString('thành công', $result['content'][0]['text']);
// Clean up
@unlink(content_path("posts/vi/" . now()->format('Y/m/d') . "-test-post.md"));
}
}
Kết Luận
MCP là USB-C của AI integrations — một connector tiêu chuẩn cho tất cả AI clients. Build MCP server cho Laravel app cần ít effort và cho bạn:
- Claude Desktop có thể search blog, đọc posts, tạo drafts
- VS Code Copilot truy cập context codebase
- Cursor tương tác với application data
- Bất kỳ MCP client tương lai hoạt động tự động
Bắt đầu thế nào:
- Cài
modelcontextprotocol/php-sdk - Tạo 2-3 read-only tools (search, get, list) — an toàn, low risk
- Test với Claude Desktop hoặc VS Code
- Thêm resources cho data AI cần đọc
- Thêm write tools khi comfortable với security model
- Thêm prompts cho workflows thường dùng