раздел 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 - реальный риск.

Полезные ссылки