Model Context Protocol (MCP) trong PHP: Kết Nối AI với Ứng Dụng

· 12 min read

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:

  1. Cài modelcontextprotocol/php-sdk
  2. Tạo 2-3 read-only tools (search, get, list) — an toàn, low risk
  3. Test với Claude Desktop hoặc VS Code
  4. Thêm resources cho data AI cần đọc
  5. Thêm write tools khi comfortable với security model
  6. Thêm prompts cho workflows thường dùng

Bình luận