Chuỗi: cli-to-agent-native · Phần 7
Năng suất & công cụ dev
Biến CLI thành MCP Server: agent tự discover và invoke tool của bạn
Wrap CLI adapter thành MCP server trong 20 dòng code. Trade-off thực: 55,000 tokens upfront vs zero tokens cho subprocess CLI. Khi nào dùng MCP, tại sao Perplexity CTO rời bỏ MCP.
2026-03-177 phút đọcVI
- 1.Sự tiến hóa giao diện: GUI → CLI → API → Agent
- 2.Giải phẫu CLI cho agent: 7 nguyên tắc thiết kế
- 3.API trước, CLI sau: Adapter Pattern — tầng trung gian tạo nên CLI chuyên nghiệp
- 4.Từ API spec đến structured metadata: nguồn sự thật cho code generation
- 5.Code Generation Pipeline: Từ metadata JSON đến 16,000 dòng code trong vài giây
- 6.Test generated code: 4 chiến lược cho code mà không ai viết tay
- 7.Biến CLI thành MCP Server: agent tự discover và invoke tool của bạn(bài này)
- 8.Agent Skills: giảm 99.6% token cost so với MCP
- 9.Progressive Enhancement: 5 levels từ raw API đến agent-native — khi nào dùng level nào
- 10.End-to-End Build: Từ 439 API endpoints đến agent-ready CLI — case study: một procurement CLI
English title: Making Your CLI Agent-Discoverable: MCP Server
Mở đầu
Tháng 11 năm 2024, Anthropic publish Model Context Protocol (MCP) — một open protocol để standardize cách AI models connect với external tools. Ba tháng sau, OpenAI adopt MCP. Đến tháng 3 năm 2025, MCP có 5,000+ community servers và trở thành de facto standard cho AI tool integration.
Nhưng cùng thời điểm đó, Perplexity CTO đăng một thread dài giải thích tại sao họ không dùng MCP — và thay vào đó build CLI-based tool interface. Lý do: token cost.
Bài này đi vào cả hai phía: cách implement MCP server (dễ hơn bạn nghĩ), và khi nào MCP có nghĩa — khi nào không.
1. MCP là gì, thực sự
MCP không phải magic. Đó là protocol chuẩn hóa 3 thứ:
- Tool discovery: Agent hỏi "mày có những tool nào?" → server trả JSON list
- Tool schema: Agent hỏi "tool X nhận input gì?" → server trả JSON schema
- Tool invocation: Agent gọi
tool_X(params)→ server execute và trả kết quả
Nếu bạn đã có adapter (bài 3), bước 1-2 là đọc metadata. Bước 3 là gọi adapter method.
Agent
│ 1. tools/list
│ 2. tools/call {name, params}
▼
MCP Server
│ 3. execute
▼
Adapter (business logic)
│ 4. HTTP call
▼
REST API
Protocol transport: JSON-RPC 2.0 qua stdio (subprocess) hoặc SSE (HTTP).
2. Implement MCP server trong 20 dòng
Với Python MCP SDK (Anthropic):
# mcp_server.py
import json
import os
from dataclasses import asdict
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
from adapters.user_adapter import UserAdapter, AdapterError
server = Server("user-management-mcp")
def get_adapter() -> UserAdapter:
token = os.environ.get("API_TOKEN", "")
return UserAdapter(token=token)
@server.list_tools()
async def list_tools() -> list[types.Tool]:
"""Declare available tools — agent sẽ receive list này khi connect."""
return [
types.Tool(
name="list_users",
description="List all users. Supports filtering by role.",
inputSchema={
"type": "object",
"properties": {
"role": {
"type": "string",
"enum": ["admin", "member", "viewer"],
"description": "Filter by role (optional)",
}
},
},
),
types.Tool(
name="get_user",
description="Get a specific user by ID.",
inputSchema={
"type": "object",
"properties": {
"user_id": {"type": "integer", "description": "User ID"}
},
"required": ["user_id"],
},
),
types.Tool(
name="ensure_user",
description="Create user if not exists. Idempotent — safe to call multiple times.",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"role": {"type": "string", "enum": ["admin", "member", "viewer"], "default": "member"},
},
"required": ["name", "email"],
},
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
"""Execute tool và trả kết quả dưới dạng JSON string."""
try:
with get_adapter() as adapter:
if name == "list_users":
users = list(adapter.list_users(role=arguments.get("role")))
result = [asdict(u) for u in users]
elif name == "get_user":
user = adapter.get_user(arguments["user_id"])
result = asdict(user)
elif name == "ensure_user":
user, action = adapter.ensure_user(
name=arguments["name"],
email=arguments["email"],
role=arguments.get("role", "member"),
)
result = {"action": action, "user": asdict(user)}
else:
result = {"error": "unknown_tool", "tool": name}
except AdapterError as e:
result = {"error": e.code, "message": str(e), "exit_code": e.exit_code}
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())Đăng ký trong Claude Desktop / Claude Code settings.json:
{
"mcpServers": {
"user-management": {
"command": "python",
"args": ["mcp_server.py"],
"env": {
"API_TOKEN": "${API_TOKEN}"
}
}
}
}Xong. Agent bây giờ có thể discover và call list_users, get_user, ensure_user mà không cần biết đây là CLI hay API.
3. Auto-generate MCP server từ metadata
Nếu bạn đã có metadata JSON (bài 4) và pipeline (bài 5), generate MCP server là thêm một template:
{# templates/mcp_server.py.j2 #}
"""Auto-generated MCP server. DO NOT EDIT MANUALLY."""
import json
import os
from dataclasses import asdict
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
{% for resource in resources %}
from adapters.{{ resource }}_adapter import {{ resource | title }}Adapter, AdapterError
{% endfor %}
server = Server("{{ api_name }}-mcp")
TOOLS = [
{% for method in methods %}
types.Tool(
name="{{ method.id | replace('.', '_') }}",
description="{{ method.summary }}",
inputSchema={
"type": "object",
"properties": {
{% for param in method.parameters %}
"{{ param.name }}": {
"type": "{{ param.type }}",
{% if param.enum %}"enum": {{ param.enum | tojson }},{% endif %}
"description": "{{ param.description }}",
},
{% endfor %}
},
"required": [{% for p in method.parameters if p.required %}"{{ p.name }}"{% if not loop.last %}, {% endif %}{% endfor %}],
},
),
{% endfor %}
]Thêm vào Makefile:
codegen: extract normalize
python pipeline/generator.py $(METADATA) $(GENERATED) $(TEMPLATES)
python pipeline/generate_mcp.py $(METADATA) mcp_server.py # ← new4. Trade-off thực sự: MCP vs CLI
Đây là phần quan trọng nhất của bài.
Khi agent connect đến MCP server, toàn bộ schema của tất cả tools được load vào context ngay lập tức. Không lazy-load, không on-demand. Tất cả một lần.
User message
│
▼ (before agent even starts)
Load MCP tools schema: tool_1 schema, tool_2 schema, ..., tool_N schema
= ~55,000 tokens cho một MCP server trung bình (30-50 tools)
Agent context window: thường 200,000 tokens (Claude 3.5)
└── MCP overhead: 55,000 tokens = 27.5% context đã bị chiếm
So với CLI:
User message
│
▼
Agent quyết định dùng tool X
│
▼ subprocess.run(["myapp", "list-users", "--json"])
Cost: 0 tokens upfront
Data thực tế từ benchmark:
| Approach | Token cost | Task completion |
|---|---|---|
| MCP Server (full schema) | ~55,000 tokens upfront | Baseline |
| CLI via subprocess | ~0 tokens | +28% vs MCP |
| Skills doc (SKILL.md) | ~200-500 tokens on-demand | ~similar to CLI |
28% higher task completion không phải vì MCP tệ — mà vì context window là zero-sum. 55,000 tokens dùng cho schema = 55,000 tokens ít hơn cho actual task context.
5. Khi nào dùng MCP
MCP có nghĩa khi:
Rich discovery cần thiết: Agent phải explore nhiều tools không biết trước. MCP schema cho agent biết tool nào có thể relevant mà không cần user hint.
Interactive workflow: User và agent đang làm việc interactive, nhiều tool calls trong một session. Schema load một lần, dùng nhiều lần.
IDE integration: Claude Code, Cursor — tools được expose qua MCP cho IDE-level integration. IDE pre-loads schema, user không chịu token cost mỗi message.
Stateful tools: Tool cần maintain state giữa calls (database connection pool, session). MCP server lifecycle phù hợp hơn subprocess.
MCP không có nghĩa khi:
- Agent chỉ cần 1-2 tools specific → CLI subprocess rẻ hơn nhiều
- Task thuần automation (không interactive) → CLI trong bash script
- Context window là bottleneck → 55k token overhead không chấp nhận được
- Tool đơn giản (CRUD, không stateful) → Skills doc + CLI đủ
6. Hybrid approach: MCP + CLI
Thực tế tốt nhất: dùng cả hai, chọn per-task:
# agent_workflow.py
import subprocess
import json
from typing import Any
def call_cli(command: list[str]) -> dict:
"""Gọi CLI tool — zero token overhead, sync."""
result = subprocess.run(command + ["--json"], capture_output=True, text=True)
if result.returncode != 0:
error = json.loads(result.stderr) if result.stderr else {"error": "unknown"}
return {"success": False, **error, "exit_code": result.returncode}
return {"success": True, "data": json.loads(result.stdout)}
# Simple lookup → CLI (fast, no MCP overhead)
user = call_cli(["myapp", "get-user", str(user_id)])
# Complex workflow → MCP tools (rich discovery, stateful)
# Agent với MCP tools sẽ orchestrate qua nhiều tool callsPattern này là Progressive Enhancement — bài 9 sẽ đi sâu vào đây.
7. Ứng dụng trong AI-centric engineering
MCP protocol sẽ tiếp tục evolve. Anthropic đang add authentication (OAuth 2.0 support), streaming responses, và binary data transport. OpenAI, Google đều đang implement compatibility.
Nhưng underlying principle sẽ không thay đổi: agent cần structured interface để discover và invoke tools. Cho dù interface đó là MCP, gRPC, REST hay CLI — câu hỏi quan trọng là token economics và complexity fit cho use case.
Adapter pattern (bài 3) là lý do bạn có thể serve cùng business logic qua CLI, MCP, và Python SDK mà không duplicate code. Khi bạn cần switch từ CLI sang MCP cho một workflow — bạn chỉ cần thêm 20 dòng wrapper, không rewrite business logic.
Bài tiếp
Bài 8: Skills — The Lightweight Alternative (99.6% Token Reduction) — Skills doc là static markdown mô tả tool, workflow, và examples. Chỉ 200-500 tokens on-demand, so với MCP 55,000 tokens upfront. Block Engineering gọi đây là pattern tương lai. Code ví dụ: generate SKILL.md từ metadata trong pipeline.
