раздел 07 · подстраница 4
Свой MCP-сервер
Своя MCP-обёртка над внутренним API - 50 строк кода. Покажу минимальный пример на Python и на TypeScript: один tool hello, который возвращает приветствие. Дальше расширяете под свои нужды.
Зачем
Свой MCP пишут когда:
- Внутренний API компании, которого нет в open-source каталоге.
- Сложная интеграция с несколькими сервисами в один tool.
- Нужны кастомные tools под специфический workflow (расчёт метрик, генерация отчётов).
- Контроль безопасности - не хотите давать Claude прямой доступ к БД, но даёте обёртку с проверками.
Вариант 1. Python через mcp SDK
Установить:
pip install mcp
my_mcp.py:
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
server = Server("my-mcp")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="hello",
description="Поздороваться с пользователем по имени.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Имя для приветствия"
}
},
"required": ["name"],
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "hello":
user_name = arguments.get("name", "stranger")
return [TextContent(type="text", text=f"Привет, {user_name}!")]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
Регистрируем в Claude:
claude mcp add my-mcp --transport stdio --scope local \
-- python3 /absolute/path/to/my_mcp.py
Проверка:
claude mcp list
claude
> /mcp
> Поздоровайся со мной, меня зовут Павел
# Claude вызывает tool hello → возвращает "Привет, Павел!"
Вариант 2. TypeScript через @modelcontextprotocol/sdk
Инициализация:
mkdir my-mcp && cd my-mcp
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
npx tsc --init
src/server.ts:
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "my-mcp", version: "0.1.0" },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "hello",
description: "Поздороваться с пользователем по имени.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Имя для приветствия" },
},
required: ["name"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "hello") {
const userName = (request.params.arguments?.name as string) ?? "stranger";
return {
content: [{ type: "text", text: `Привет, ${userName}!` }],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
Скомпилировать и запустить:
npx tsc
chmod +x dist/server.js
package.json добавьте:
{
"bin": {
"my-mcp": "dist/server.js"
},
"type": "module"
}
Регистрируем:
claude mcp add my-mcp --transport stdio --scope local \
-- node /absolute/path/to/my-mcp/dist/server.js
Тестирование без Claude
Удобно проверить, что сервер вообще работает, до подключения к Claude. MCP по stdio - принимает JSON-RPC сообщения. Простейший тест:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 my_mcp.py
Должно вывести JSON со списком tools.
Альтернатива - официальный MCP Inspector:
npx @modelcontextprotocol/inspector python3 /path/to/my_mcp.py
Открывает локальный UI в браузере, где можно дёргать tools и смотреть ответы.
Расширяем до полезного
Минимальный пример превращаем в обёртку над внутренним API:
import os
import httpx
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "search_customers":
query = arguments["query"]
api_key = os.environ["INTERNAL_API_KEY"]
async with httpx.AsyncClient() as client:
r = await client.get(
"https://api.internal.company/customers",
params={"q": query},
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
r.raise_for_status()
return [TextContent(type="text", text=r.text)]
Регистрация с env:
claude mcp add company-api --transport stdio --scope user \
--env INTERNAL_API_KEY="$INTERNAL_API_KEY" \
-- python3 /path/to/my_mcp.py
Полезные паттерны
- Read-only по умолчанию: первый tool - только чтение. Write-tools пишите явно и опасно для Claude явно сигнализируйте через
description. - Валидация на входе: проверяйте
argumentsперед вызовом API. Claude иногда передаёт чушь. - Понятные ошибки: вместо
raise ExceptionвозвращайтеTextContentс описанием, что пошло не так - Claude увидит и поправит запрос. - Логирование в stderr: stdout зарезервирован под JSON-RPC. Логи - только в stderr (
print(..., file=sys.stderr)).
Антипаттерны
- Печатать что угодно в stdout кроме JSON-RPC ответов - сервер сломается, Claude отвалится с парс-ошибкой.
- Tool без
inputSchema- Claude не поймёт, что передавать. - Слишком общий description ("делает запросы") - Claude не подцепит инструмент в нужный момент.
- Прокидывать сырые SQL/shell аргументы без валидации - инъекции через MCP - реальный риск.
Полезные ссылки
- Python MCP SDK - официальный
- TypeScript MCP SDK - официальный
- MCP specification - спецификация протокола
- MCP Inspector - тестирование без Claude