# Сколько инструментов должно быть у MCP-сервера: замеры от 5 до 100

> AI Release · @ai_release1 · https://ai-release.org/guides/skolko-instrumentov-u-mcp-servera-zamery.html

## TL;DR

- Время выполнения: 40 минут
- Уровень сложности: Intermediate
- Понадобится: Python 3.11+, пакет `mcp` (Python SDK, версию не фиксируем — сверьтесь с документацией), `pip`, `bash`, MCP-клиент (IDE или чат с поддержкой MCP), ключ API модели
- Результат: файл `measurements.jsonl` с 5 строками замеров (N = 5, 10, 20, 50, 100) и обоснованное число инструментов для вашего сервера

Термины: MCP (Model Context Protocol) — открытый протокол, по которому клиент (IDE, чат) подключается к серверу и получает каталог инструментов (tools) — функций, которые модель может вызывать. Обмен идёт сообщениями JSON-RPC 2.0, по stdio (stdin/stdout процесса) или по HTTP. Каталог инструментов целиком уезжает в контекст модели при каждом запросе, поэтому N инструментов — это N описаний в каждом вызове.

## Требования и подготовка
![Иллюстрация к разделу «Требования и подготовка»](https://ai-release.org/guides/img/skolko-instrumentov-u-mcp-servera-zamery-ru-1.jpg?v=1793343131)


- ОС: Linux, macOS или Windows с WSL2; права на запуск процессов и запись в текущую папку.
- Python 3.11+ и `pip`: `python --version`.
- SDK: `pip install "mcp[cli]"` — даёт серверный класс `FastMCP` и клиент `ClientSession`.
- `bash` (или Git Bash) для цикла замеров, `jq` для просмотра JSON: `jq --version`.
- Ключ API модели-клиента и хотя бы один MCP-клиент, куда этот ключ вводится.
- Свободное место: 1 ГБ (скрипты и логи). Локальную модель в память не грузим — но если будете, посчитайте объём по таблице [Сколько RAM и VRAM нужно для локальной ИИ-модели](https://ai-release.org/guides/localnye-ii-modeli-skolko-ram-i-vram.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

Чек-лист перед стартом:

- [ ] `python --version` показывает 3.11 или новее
- [ ] `python -c "import mcp; print(mcp.__file__)"` печатает путь к пакету
- [ ] Создана пустая рабочая папка, например `~/mcp-scale`
- [ ] Есть рабочая папка `tasks/` — её создадим в шаге 1
- [ ] Известен лимит контекста вашей модели в токенах (сверьтесь с документацией провайдера)

**Пример:** `cd ~ && mkdir mcp-scale && cd mcp-scale && python -c "import mcp; print(mcp.__file__)"` → печатает `/…/site-packages/mcp/__init__.py`. Если вместо пути ошибка `ModuleNotFoundError: No module named 'mcp'` — пакет не установился, вернитесь к `pip install`.

## Шаг 1. Соберите эталонный MCP-сервер с переключаемым числом инструментов
![Иллюстрация к разделу «Шаг 1. Соберите эталонный MCP-сервер с переключаемым числом инструментов»](https://ai-release.org/guides/img/skolko-instrumentov-u-mcp-servera-zamery-ru-2.jpg?v=1793343141)


**Что сделать:** создать сервер `mcp_scale_server.py`, у которого количество инструментов задаётся переменной окружения `TOOLS_COUNT`, и пробник `probe_measure.py`, который подключается к нему по stdio и печатает метрики.

**Зачем:** чтобы во всех прогонах менялось ровно одно — N. Если менять ещё и набор инструментов, сравнение ничего не покажет.

```python
# mcp_scale_server.py
import os
from mcp.server.fastmcp import FastMCP

N = int(os.environ.get("TOOLS_COUNT", "5"))
mcp = FastMCP("scale-probe")

def make_tool(i: int):
    def impl(topic: str) -> str:
        return f"tool-{i}: {topic}"
    return impl

for i in range(N):
    mcp.add_tool(
        make_tool(i),
        name=f"tool_{i:03d}",
        description=f"Вернуть строку tool-{i}. Используй, когда спрашивают про topic-{i}.",
    )

if __name__ == "__main__":
    mcp.run()  # по умолчанию stdio
```

Имя инструмента передаётся явно (`name=`), поэтому одинаковые имена внутренних функций `impl` не конфликтуют — на этом чаще всего спотыкаются.

```python
# probe_measure.py — один прогон: N, размер каталога, задержка
import asyncio, json, os, sys, time
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def run(n: int):
    params = StdioServerParameters(
        command=sys.executable,
        args=["mcp_scale_server.py"],
        env={**os.environ, "TOOLS_COUNT": str(n)},
    )
    async with stdio_client(params) as (r, w):
        async with ClientSession(r, w) as s:
            await s.initialize()
            t0 = time.perf_counter()
            res = await s.list_tools()
            ms = (time.perf_counter() - t0) * 1000
    catalog = json.dumps([t.model_dump() for t in res.tools], ensure_ascii=False)
    print(json.dumps({
        "n": n,
        "tools": len(res.tools),
        "catalog_chars": len(catalog),
        "catalog_tokens_est": round(len(catalog) / 4),
        "list_tools_ms": round(ms, 1),
    }))

asyncio.run(run(int(sys.argv[1])))
```

`catalog_tokens_est` — грубая оценка «символы / 4»; точное число токенов даёт только токенизатор вашей модели (сверьтесь с документацией).

**Как проверить:**

```bash
python probe_measure.py 5
```

**Пример вывода:**

```json
{"n": 5, "tools": 5, "catalog_chars": <ваш-замер>, "catalog_tokens_est": <ваш-замер>, "list_tools_ms": <ваш-замер>}
```

Главное здесь — `"tools": 5` и код возврата 0. Если `tools` меньше N, сервер поднялся не полностью.

## Шаг 2. Прогоните сетку замеров 5 → 100

**Что сделать:** выполнить пробник на пяти значениях N, затем заполнить по 20 задач на клиенте и посчитать долю верных вызовов.

**Зачем:** узкое место — не «количество инструментов» само по себе, а объём каталога в контексте и точность выбора нужного инструмента. Первое вы измеряете автоматически, второе — прогоном задач.

```bash
for n in 5 10 20 50 100; do
  python probe_measure.py "$n" >> measurements.jsonl
done
cat measurements.jsonl
```

**Пример:** после пяти итераций в файле ровно 5 строк с `"n"` от 5 до 100. Если строк меньше — соответствующая итерация упала, смотрите её stderr.

Теперь набор задач. Он одинаков для всех N и ссылается только на `topic-0 … topic-4`, поэтому существует при любом количестве инструментов:

```bash
mkdir -p tasks && python - <<'PY'
import json
rows = []
for i in range(5):
    for k in range(4):
        rows.append({"prompt": f"верни строку для topic-{i} (вариант {k})", "expect": f"tool_{i:03d}"})
open("tasks/tasks.jsonl", "w").write("\n".join(json.dumps(r, ensure_ascii=False) for r in rows))
print("tasks:", len(rows))
PY
```

Подключите сервер к клиенту. Формат записи одинаков у большинства клиентов, имя файла зависит от клиента — сверьтесь с документацией:

```json
{
  "mcpServers": {
    "scale-20": {
      "command": "python",
      "args": ["/home/<user>/mcp-scale/mcp_scale_server.py"],
      "env": { "TOOLS_COUNT": "20" }
    }
  }
}
```

`env` здесь обязателен: без него клиент запустит сервер с `TOOLS_COUNT` по умолчанию, и все прогоны дадут одно и то же число — это самая частая ошибка в таких замерах. Модель для прогона задач берите из [обзора нейросетей и моделей 2026](https://ai-release.org/guides/luchshie-nejroseti-i-modeli-2026.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide) — прогон должен идти на той же модели, с которой сервер будет работать в бою.

**Как проверить:** на каждой итерации меняйте `env.TOOLS_COUNT` (5, 10, 20, 50, 100), перезапускайте клиент (перезапуск обязателен — каталог кешируется при старте), прогоняйте 20 задач и записывайте число совпадений `expect` с фактически вызванным инструментом.

**Пример:** задача `верни строку для topic-3 (вариант 1)` при ожидаемом `tool_003`, а в логе клиента вызов `tool_003` — плюс один верный; вызов `tool_013` — ошибка выбора.

## Шаг 3. Сведите результаты и выберите число инструментов
![Иллюстрация к разделу «Шаг 3. Сведите результаты и выберите число инструментов»](https://ai-release.org/guides/img/skolko-instrumentov-u-mcp-servera-zamery-ru-3.jpg?v=1793343148)


**Что сделать:** собрать все замеры в таблицу, добавить колонку точности и выбрать N.

**Зачем:** решение принимается по двум ограничениям сразу — каталог должен влезать в бюджет контекста, а точность выбора не должна проваливаться.

```bash
python - <<'PY'
import json
rows = [json.loads(l) for l in open("measurements.jsonl")]
print(f"{'N':>4} {'tools':>6} {'tok≈':>7} {'ms':>7}")
for r in rows:
    print(f"{r['n']:>4} {r['tools']:>6} {r['catalog_tokens_est']:>7} {r['list_tools_ms']:>7}")
PY
```

**Пример вывода:**

```
   N  tools    tok≈      ms
   5      5  <замер>  <замер>
  10     10  <замер>  <замер>
  20     20  <замер>  <замер>
  50     50  <замер>  <замер>
 100    100  <замер>  <замер>
```

Заполните сводную таблицу (числа — свои; в примере только структура):

| N инструментов | tools в `list_tools` | Каталог, токенов ≈ | `list_tools`, мс | Верных вызовов из 20 |
|---|---|---|---|---|
| 5 | 5 | `<замер>` | `<замер>` | `<0–20>` |
| 10 | 10 | `<замер>` | `<замер>` | `<0–20>` |
| 20 | 20 | `<замер>` | `<замер>` | `<0–20>` |
| 50 | 50 | `<замер>` | `<замер>` | `<0–20>` |
| 100 | 100 | `<замер>` | `<замер>` | `<0–20>` |

Правило выбора: берите максимальное N, при котором доля верных вызовов не ниже порога, заданного заранее (например, 0,9), а `catalog_tokens_est` оставляет запас контекста под сам диалог. Всё, что выше этой точки, выносите в отдельные серверы — клиент подключает несколько серверов одновременно:

```json
{
  "mcpServers": {
    "scale-core": { "command": "python", "args": ["/home/<user>/mcp-scale/mcp_scale_server.py"], "env": { "TOOLS_COUNT": "20" } },
    "scale-extra": { "command": "python", "args": ["/home/<user>/mcp-scale/mcp_scale_server.py"], "env": { "TOOLS_COUNT": "10" } }
  }
}
```

**Как проверить:** финальный чек-лист.

- [ ] В `measurements.jsonl` ровно 5 строк, значения `n` совпадают с 5, 10, 20, 50, 100
- [ ] `tools` в каждой строке равно `n`
- [ ] Для каждого N заполнена колонка верных вызовов
- [ ] Выбранное N\* записано в `env.TOOLS_COUNT` рабочего конфига
- [ ] Повторный `python probe_measure.py <N*>` даёт тот же `tools`

## Возможные проблемы и решения

**Проблема. Клиент не видит ни одного инструмента, пробник зависает**

- Ошибка: `Error: Unexpected token 'I', "INFO ..." is not valid JSON`
- Причина: в сервер добавлен `print()` или логгер пишет в stdout. По stdio-транспорту stdout занят сообщениями JSON-RPC, любая посторонняя строка ломает поток.
- Решение: увести логи в stderr и убрать `print()` из сервера:

```python
import logging, sys
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
```

Проверка: `python probe_measure.py 5` снова печатает `"tools": 5`, а лог виден в stderr.

**Проблема. При 100 инструментах модель называет инструмент, которого нет**

- Ошибка: `Error: Tool tool_112 not found`
- Причина: каталог разросся, описания `tool_000 … tool_099` отличаются одним числом; модель путает соседние имена. Это и есть измеряемая деградация точности, а не сбой протокола.
- Решение: уменьшить N до значения из шага 3, где доля верных вызовов была приемлемой, и разнести остаток по второму серверу (`scale-extra`). Проверка: `curl` не нужен, достаточно повторить прогон 20 задач на новом N.

**Проблема. Запрос падает на этапе генерации, хотя `list_tools` работает**

- Ошибка: `Error: 400 prompt is too long` (текст у разных провайдеров отличается — сверьтесь с документацией)
- Причина: каталог из 100 описаний плюс история диалога не влезают в лимит контекста модели.
- Решение: сравните `catalog_tokens_est` для 100 и для 20 из `measurements.jsonl`, выставьте `TOOLS_COUNT` по бюджете модели и перезапустите клиент. Если инструменты нужны все — подключайте их отдельными серверами и держите в основном только часто используемые.

## Резюме и следующие шаги

Оптимальное число инструментов MCP-сервера определяется двумя замерами: объёмом каталога в токенах и долей верных вызовов на фиксированном наборе задач. Прогоните сетку 5–100 на своих реальных инструментах и зафиксируйте N\*, при котором точность держится выше порога, а каталог оставляет запас контекста.

## FAQ

**Сколько инструментов должно быть у MCP-сервера?**
Столько, сколько проходит ваш собственный замер: каталог влезает в бюджет контекста, а доля верных вызовов не ниже заданного порога. Универсального числа нет — оно зависит от модели и от того, насколько похожи описания инструментов.

**Что такое MCP простыми словами?**
Протокол, по которому LLM-клиент получает от сервера список функций (tools) и вызывает их. Клиент и сервер общаются сообщениями JSON-RPC 2.0, чаще всего через stdin/stdout дочернего процесса.

**Почему много инструментов замедляет работу?**
Каждое описание инструмента уходит в контекст при каждом запросе: растёт число входных токенов, а вместе с ним задержка и риск переполнения контекста. Плюс модели труднее выбрать нужный инструмент среди похожих имён.

**Как уменьшить число инструментов без потери функций?**
Разнести их по нескольким серверам в одном конфиге клиента, убрать дублирующие по смыслу инструменты и оставить в основном сервере только часто используемые. Если нужен бесплатный вариант клиента для прогонов, см. [рабочие способы пользоваться ИИ-моделями бесплатно](https://ai-release.org/guides/kak-polzovatsya-ii-besplatno.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).
