# Дублирующиеся регистрации MCP-серверов запускаются дважды?

Дублирующаяся регистрация MCP редко бывает безобидным конфигурационным мусором. Когда две записи описывают один сервер под разными именами, агент получает два маршрута к одной возможности. Для stdio это часто означает два дочерних процесса. Для удаленной конечной точки это может означать два аутентифицированных подключения, два набора инструментов и два независимых места, где агент способен выбрать неправильный вызов.

Неприятность в том, что приоритет конфигураций не решает этот класс проблем. Приоритет определяет только, что произойдет, когда записи конфликтуют по имени. Он не показывает, что `repo-api`, `internal-api` и `my-api` могут быть тремя названиями одного исполняемого файла и одной учетной записи. Считайте идентичность регистрации операционной задачей, а не вопросом именования.

## Дублирующиеся регистрации создают отдельные пути выполнения

Две разные записи MCP-серверов могут дважды запустить одно и то же, потому что клиент воспринимает регистрации как определения подключений, а не как псевдонимы, которые нужно объединять. Спецификация транспорта MCP говорит, что клиент запускает stdio-сервер как дочерний процесс и обменивается JSON-RPC через стандартные ввод и вывод этого процесса. Если две настроенные записи вызывают одну и ту же команду, обычным результатом становятся два дочерних процесса, каждый со своей инициализацией и временем жизни.

Это не значит, что агент механически вызовет каждый инструмент дважды. Модели сами решают, какой доступный инструмент вызвать. На практике риск серьезнее: агент может увидеть похожие по описанию инструменты из обеих регистраций, вызвать один в первом сообщении, а другой при повторной попытке, или использовать оба, потому что их имена намекают на разные обязанности. При этом запуск сервера уже может иметь побочные эффекты до первого вызова инструмента.

Я встречал серверы, которые казались пассивными, пока не изучишь путь инициализации. Они обновляют токен доступа, создают каталог кэша, открывают локальную базу SQLite, запускают цикл опроса для поддержания индекса в актуальном состоянии или регистрируют потребителя вебхуков. Ни один из этих вариантов не нарушает MCP. Проблема появляется, когда команда считает, что «MCP-сервер» это один общий объект без состояния.

Удаленный сервер меняет форму сбоя, но не отменяет необходимости его предотвращать. Streamable HTTP рассчитан на независимый серверный процесс, способный обрабатывать несколько подключений клиентов. Это удобно, когда несколько клиентов подключаются намеренно. Но сервис может увидеть два подключения, которые оба выдают себя за агента одного разработчика, если у сервиса нет надежного способа различать и ограничивать их.

Поэтому первый диагностический вопрос звучит просто: создают ли эти две записи два пути выполнения к одному внешнему источнику полномочий? Если да, это дублирование, даже когда JSON отличается, а имена выглядят разумно.

## Имя сервера не равно его идентичности

У регистрации MCP как минимум две идентичности, и команды часто смешивают их.

**Отображаемая идентичность** это настроенное имя, например `repo-api` или `staging-db`. Оно важно, потому что клиент использует его для показа инструментов и разрешения конфликтов конфигурации. Это имя для людей и внутреннего учета клиента.

**Идентичность выполнения** это то, к чему регистрация действительно обращается: исполняемый файл с аргументами и нужным окружением либо удаленный URL с контекстом аутентификации. Она важна для процессов и внешних систем.

В аккуратной конфигурации эти идентичности совпадают. Но это не обязательно. Именно предположение об их совпадении позволяет дубликатам проходить проверку.

Рассмотрим такие записи:

```json
{
  "mcpServers": {
    "billing": {
      "command": "python3",
      "args": ["tools/billing_mcp.py", "--account", "prod"]
    },
    "finance-tools": {
      "command": "python3",
      "args": ["tools/billing_mcp.py", "--account", "prod"]
    }
  }
}
```

Имена отличаются, но программа и аргументы одни и те же. Если сама программа не гарантирует наличие только одного экземпляра, это два запуска.

Теперь рассмотрим менее очевидный вариант:

```json
{
  "mcpServers": {
    "deploy": {
      "command": "./bin/deploy-mcp",
      "args": ["--workspace", "/Users/dev/work/acme"]
    },
    "release-helper": {
      "command": "node",
      "args": ["scripts/mcp-launch.js", "deploy", "--workspace", "/Users/dev/work/acme"]
    }
  }
}
```

При сравнении текста эти записи различаются. При сравнении на уровне процессов может оказаться, что оболочка запускает тот же исполняемый файл `deploy-mcp` с тем же рабочим каталогом. Поэтому при проверке конфигурации нужно правило идентичности, а не простая проверка одинаковых строк.

Сравнивайте записи в таком порядке:

1. Сравните нормализованную удаленную конечную точку или конечный исполняемый файл, который запускает команда.
2. Сравните аргументы, выбирающие учетную запись, арендатора, репозиторий, рабочее пространство или цель записи.
3. Сравните рабочий каталог и имена несекретных переменных окружения, которые меняют поведение.
4. Отдельно сравните владельца учетных данных. Две записи, обращающиеся к одной конечной точке от имени разных субъектов, не являются безобидными дубликатами. Это решение о правах доступа, которому нужно обоснование.

Не сравнивайте секретные значения во время аудита. Они вам не нужны, а их копирование в результаты проверки создает вторую проблему безопасности. Зафиксируйте, что одна запись использует `BILLING_TOKEN`, а другая `PERSONAL_BILLING_TOKEN`, затем выясните, дают ли эти переменные доступ к одной учетной записи.

## Приоритет областей не удаляет записи с разными именами

Claude Code описывает три области MCP: local, project и user. Записи области проекта хранятся в файле `.mcp.json` репозитория, а записи пользовательской области доступны во всех проектах. В документации также сказано, что одинаковое имя сервера разрешается сначала в local, затем в project и потом в user. В более ранней документации пользовательская область называлась «global».

Такое поведение защищает от одного узкого случая: одинаковое имя появляется в нескольких областях. Но оно не защищает от обычной дублирующейся регистрации:

```text
User scope:    personal-git      -> /Users/dev/bin/git-mcp
Project scope: repository-git    -> /Users/dev/bin/git-mcp
```

Оба имени могут оставаться видимыми, потому что конфликта имен нет. Оба сервера могут запуститься и предоставить почти одинаковые инструменты.

Есть и другая ловушка. Разработчик видит в файле проекта `repository-git`, добавляет `personal-git` в пользовательскую область, потому что хочет использовать инструмент за пределами этого репозитория, а затем забывает, что пользовательская запись тоже загружается внутри репозитория. Сначала все работает удобно. Проблема обнаруживается позже, когда вызов инструмента записывает два аудиторских события или фоновый процесс блокирует один и тот же каталог состояния.

Используйте области для распределения ответственности, а не ради удобства:

- Помещайте регистрацию в область проекта, когда она нужна репозиторию, а конфигурацией безопасно делиться.
- Помещайте ее в пользовательскую область, когда это личный инструмент, который должен работать в разных репозиториях.
- Используйте local для личного эксперимента, связанного с конкретным репозиторием, который не нужно коммитить.
- Не дублируйте запись проекта в пользовательской области. Если она нужна в другом месте, вызывайте ее только там, где действует конфигурация проекта, или создайте намеренно отдельную запись с другой целью и документированным назначением.

На конфликт с одинаковым именем тоже стоит обратить внимание. Он не создает две активные записи так, как это делают два разных имени, но может скрыть командную конфигурацию за личной. Тогда агент выполняет действия с личным исполняемым файлом или личной конечной точкой, а проверяющие считают, что действует определение из репозитория. Это ошибка происхождения конфигурации, а не количества процессов, но ее тоже нужно исправить.

## Докажите дублирование на пути от конфигурации к процессу и вызову

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

Начните в нужном репозитории:

```sh
claude mcp list
claude mcp get repository-git
claude mcp get personal-git
```

Anthropic называет `claude mcp list`, `claude mcp get` и `claude mcp remove` стандартными командами управления. Используйте вывод, чтобы найти все видимые имена, затем проверяйте подозрительные записи по одной.

Для каждой записи зафиксируйте в черновом файле пять фактов: настроенное имя, область, команду или URL, аргументы и внешнюю учетную запись или рабочее пространство, к которым она обращается. Не вставляйте значения переменных окружения. Для удаленного сервера сохраните хост и путь, но не заголовок авторизации.

Затем запустите короткую сессию агента и проверьте процессы, пока он подключен. В macOS или Linux замените `billing_mcp.py` уникальной частью ожидаемой команды:

```sh
ps -ax -o pid,ppid,lstart,command | grep '[b]illing_mcp.py'
```

Дублирующийся запуск stdio может выглядеть так:

```text
91204 91188 Tue Jul 21 10:14:07 2026 python3 tools/billing_mcp.py --account prod
91219 91188 Tue Jul 21 10:14:09 2026 python3 tools/billing_mcp.py --account prod
```

Идентификаторы процессов различаются. Родительским процессом может быть один и тот же процесс агента или два связанных процесса агента. Важно другое: обе команды имеют одинаковую идентичность выполнения и перекрывающиеся периоды работы.

Затем выполните один намеренно безопасный вызов инструмента только для чтения. Выберите вызов с узким ожидаемым результатом, например получение текущего идентификатора учетной записи или списка одного известного объекта. Проверьте журналы целевой системы, журнала сервера или журнал действий. Если вы видите два независимых подключения, но один вызов, значит, обнаружили дублирующийся запуск сервера. Если вызовов два, выясните, выбрал ли агент два инструмента, повторил ли запрос после ошибки или сервер сам выполнил работу повторно. Это разные причины, и исправлять их нужно по-разному.

Спецификация жизненного цикла MCP требует инициализации до обычной работы. Два события инициализации уже доказывают наличие двух подключений. Но они не доказывают, что произошло бизнес-действие, поэтому не сообщайте участникам инцидента, что «развертывание выполнилось дважды», только потому, что увидели два рукопожатия.

## Создавайте отпечатки конфигураций без чтения учетных данных

Полезная проверка дубликатов создает стабильный отпечаток каждой настроенной записи и исключает секретные значения. Следующий скрипт читает один или несколько JSON-файлов конфигурации, извлекает `mcpServers` и сравнивает транспорт, команду, аргументы, URL, рабочий каталог и имена переменных окружения. Передавайте ему только файлы, которые вы имеете право просматривать.

```python
#!/usr/bin/env python3
# save as mcp_duplicates.py
import hashlib
import json
import pathlib
import sys
from collections import defaultdict

if len(sys.argv) < 2:
    raise SystemExit("usage: mcp_duplicates.py CONFIG [CONFIG ...]")

entries = defaultdict(list)

for raw_path in sys.argv[1:]:
    path = pathlib.Path(raw_path).expanduser()
    with path.open() as handle:
        document = json.load(handle)

    for name, server in document.get("mcpServers", {}).items():
        identity = {
            "type": server.get("type", "stdio"),
            "command": server.get("command"),
            "args": server.get("args", []),
            "url": server.get("url"),
            "cwd": server.get("cwd"),
            "env_names": sorted(server.get("env", {}).keys()),
            "header_names": sorted(server.get("headers", {}).keys()),
        }
        encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"))
        fingerprint = hashlib.sha256(encoded.encode()).hexdigest()[:12]
        entries[fingerprint].append((str(path), name, identity))

for fingerprint, matches in sorted(entries.items()):
    if len(matches) < 2:
        continue
    print(f"DUPLICATE EXECUTION IDENTITY {fingerprint}")
    for path, name, identity in matches:
        print(f"  {path}: {name}")
        print(f"    {json.dumps(identity, sort_keys=True)}")
```

Запустите его для `.mcp.json` проекта и очищенного экспорта или копии пользовательской конфигурации, которую использует клиент:

```sh
python3 mcp_duplicates.py .mcp.json ~/tmp/user-mcp.json
```

Вывод должен выглядеть так:

```text
DUPLICATE EXECUTION IDENTITY 64e0e2509d8a
  .mcp.json: repository-git
    {"args":["tools/git_mcp.py"],"command":"python3","cwd":null,"env_names":["GIT_ACCOUNT"],"header_names":[],"type":"stdio","url":null}
  /Users/dev/tmp/user-mcp.json: personal-git
    {"args":["tools/git_mcp.py"],"command":"python3","cwd":null,"env_names":["GIT_ACCOUNT"],"header_names":[],"type":"stdio","url":null}
```

Эта проверка намеренно консервативна. Она отмечает записи с одинаковой заявленной формой выполнения. Она не доказывает, что две разные оболочки не сходятся к одному процессу, и не доказывает, что два разных URL не ведут к одному сервису. Считайте ее результат списком для проверки, а не автоматическим списком удаления.

Также возможны пропуски, когда одна конфигурация использует относительный путь, а другая абсолютный. Перед сравнением нормализуйте пути, если команда использует оба варианта. Делайте это контролируемым скриптом, который знает корень репозитория. Не выполняйте массовую замену по конфигурационным файлам.

## Два независимых экземпляра могут по-разному видеть состояние

Самые дорогие сбои не всегда связаны с дублирующимися вызовами API. Два экземпляра могут по-разному видеть локальное состояние, хотя каждый действует ровно так, как задумал его автор.

Представьте сервер, который хранит локальный кэш метаданных репозитория. Экземпляр A запускается со старой копией и записывает данные кэша в общий каталог по умолчанию. Экземпляр B запускается после переключения ветки, читает тот же каталог и считает кэш актуальным, потому что файл существует. Теперь вызов инструмента возвращает данные, которые не соответствуют текущему представлению ни одного процесса. После этого агент может выполнить совершенно корректный вызов для устаревшего объекта.

Другой знакомый сбой связан с потребителем очереди. Оба экземпляра аутентифицируются как один субъект и опрашивают один поток заданий. Если очередь допускает доставку «как минимум один раз», защита от дубликатов уже может быть предусмотрена. Если автор инструмента добавляет локальную карту дедупликации, каждый процесс получает собственную карту. Она предотвращает повторы внутри одного процесса, но ничего не делает для пары процессов.

Плохая рекомендация в такой ситуации звучит так: «Просто сделайте сервер без состояния». Она популярна, потому что кажется безопасной, а серверы без состояния на HTTP хорошо работают с множеством подключений. Но для локальных инструментов, которые намеренно хранят кэши, состояние обновления OAuth, наблюдателей за файлами или дескрипторы операций, это неверно. Требование должно быть точнее: документируйте, поддерживаются ли параллельные экземпляры, какие ресурсы они используют совместно и что происходит, когда два экземпляра работают с одной идентичностью.

Попросите владельцев серверов ответить на эти вопросы в README или выводе при запуске:

- Записывает ли запуск локальные файлы, обновляет учетные данные или запускает фоновую задачу?
- Могут ли два процесса использовать одно рабочее пространство, учетную запись и каталог кэша?
- Содержит ли каждый вызов инструмента ключ идемпотентности, когда он меняет внешнюю систему?
- Может ли оператор определить процесс клиента или сессию, создавшую запись?

Если ответ на второй вопрос отрицательный, сделайте конфликт явным. Используйте блокировку операционной системы, отдельный каталог выполнения для каждого процесса или серверную аренду. Не полагайтесь на то, что люди будут помнить о необходимости настроить сервер один раз.

## Дублирование инструментов и действий это разные инциденты

Клиент может предоставить два похожих инструмента, не выполняя ни один из них дважды. Он также может повторить внешнее действие через единственный инструмент. Расследование сбивается с пути, когда оба случая называют «дублированием MCP».

**Дублирование доступных инструментов** означает, что две регистрации объявляют пересекающиеся возможности. Агент может увидеть `billing_get_invoice` от двух серверов. Это риск конфигурации и формирования запросов. Исправьте регистрации и описания.

**Дублирование выполнения** означает, что существуют два локальных процесса или две удаленные сессии. Это риск подключения и жизненного цикла. Исправьте путь регистрации, поведение сервера при параллельной работе или и то и другое.

**Повторное внешнее действие** означает, что целевая система получила больше одного значимого запроса. Причиной могут быть дублирование доступных инструментов, логика повторных попыток, тайм-ауты, действия пользователя, поведение сервера или ошибка клиента. Докажите это идентификатором операции в целевой системе, а не выводом из количества процессов MCP.

Во время инцидента храните эти записи вместе:

```text
Agent run ID:          run-7f3a
Configured name:       repository-git
Server process ID:     91204
MCP connection start:  2026-07-21T10:14:07Z
Tool request ID:       58
Target operation ID:   commit-3a8b
```

Идентификаторы не обязаны называться именно так. Они должны связывать агент, сервер и целевой сервис. Если какой-либо уровень не может создать значение для корреляции, укажите это в записи об инциденте, а не заполняйте пробел догадками по времени.

Разделение Sallyport на журнал Sessions для запусков агентов и журнал Activity для отдельных вызовов здесь особенно полезно, потому что сохраняет это различие. Вторая сессия или подключение не доказывает автоматически второе внешнее действие, это должны подтвердить записи вызовов.

## Удалите одну регистрацию, не создавая слепую зону

После обнаружения настоящего дубликата выберите каноническую регистрацию и только потом удаляйте что-либо. У канонической записи должны быть понятный владелец, предсказуемая область, проверенная команда или конечная точка и явно указанный источник учетных данных. «На моем компьютере это работало» не подходит для выбора.

Для интеграции, которой владеет команда, обычно лучше подходит запись проекта, потому что ее можно проверить вместе с кодовой базой. Не храните учетные данные в общем файле. Claude Code поддерживает подстановку переменных окружения в `.mcp.json`, в том числе в командах, аргументах, полях окружения, URL и заголовках. Это позволяет создавать общие определения без коммита токена.

Для личного инструмента, используемого в разных проектах, подходящим местом может быть пользовательская область. В таком случае удаляйте запись проекта только если репозиторию не нужно общее определение инструмента для остальных участников. Не превращайте командную зависимость в недокументированное личное требование.

После изменения выполните спокойную проверку:

1. Сохраните удаленную запись вне активной конфигурации на время теста.
2. Запустите новый процесс агента. Уже работающие процессы могут сохранять старые подключения.
3. Выполните `claude mcp list` и проверьте оставшуюся запись командой `claude mcp get <name>`.
4. Выполните один безопасный вызов только для чтения и зафиксируйте одно подключение и один запрос к целевой системе.
5. Еще раз перезапустите агент и убедитесь, что удаленная регистрация не вернулась.

Если удаление нарушило рабочий процесс, восстановите только каноническое определение и исправьте отсутствующий путь, переменную окружения или разрешения. Не возвращайте обе записи в качестве быстрого решения. Это снова создаст неоднозначность, которую вы только что потратили время на диагностику.

## Сделайте обнаружение дубликатов частью проверки конфигурации

Лучший контроль это простое правило проверки: у каждой регистрации MCP должны быть владелец, область и уникальная идентичность выполнения для ее цели. Этого достаточно, чтобы обнаружить большинство случайных дубликатов до запуска агентов.

Если команда хранит `.mcp.json` под контролем версий, добавьте проверку отпечатков в скрипт репозитория. Запускайте ее при локальной проверке и в непрерывной интеграции для общей конфигурации. Она не увидит записи пользовательской области разработчика, поэтому добавьте `claude mcp list` в инструкцию по настройке для участников, которые сообщают о странном поведении инструментов.

Для конфигурации пользовательской области ведите короткий список вне самой конфигурации. Достаточно одной строки на сервер:

```text
personal-git | user | git tooling across repositories | owner: developer
repository-git | project | repository release workflow | owner: platform team
```

Если обе строки указывают на один исполняемый файл и одну учетную запись, одну из них нужно удалить или намеренно развести их цели. Не принимайте два имени только потому, что одно из них дружелюбнее звучит в запросе.

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