| # | Issue | Priority | Effort | Project Impact | |
|---|---|---|---|---|---|
| 01 | Split monolith tools.ts (10K lines) | High | Medium | Fault Isolation | → |
| 02 | Eliminate self-call HTTP overhead | High | High | AI Response Speed | → |
| 03 | Cache system settings per session | High | Low | DB Load Reduction | → |
| 04 | Fix inconsistent error handling | High | Low | Production Bug | → |
| 05 | Streamable HTTP session memory leak | Medium | Low | Server Stability | → |
| 06 | Inconsistent delete confirmation pattern | Medium | Low | User Experience | → |
| 07 | get_workspace_language breaks architecture | Medium | Low | Silent Breakage Risk | → |
| 08 | Replace console.log with structured logging | Low | Low | PII Exposure Risk | → |
| 09 | Add input validation beyond ObjectId | Low | Medium | AI Error Recovery | → |
| 10 | Fix pagination offset vs page inconsistency | Low | Low | Broken Feature | → |
All tool definitions and handlers live in one file behind a single handleMCPToolCall function. This creates concrete project-level risks:
handleMCPToolCall function from loading, taking down all 190 tools at onceTool handlers call fetch('http://localhost:3000/api/...') for every operation — a full HTTP round-trip to reach a function running in the same process. This has two concrete project consequences:
get_contact_details (4 parallel fetches) compound this| Scenario | HTTP Overhead | Tool Calls | Wasted |
|---|---|---|---|
| Simple search | ~3-5 ms | 1 | ~5 ms |
get_contact_details | ~3-5 ms × 4 | 4 parallel | ~15 ms |
| Create invoice (auto-products) | ~3-5 ms × N | 1 + N products | ~25 ms |
| LLM session (20 tool calls) | ~3-5 ms × 20+ | 20+ | ~80 ms |
Extract controller logic into importable service functions that both API routes and MCP tools can call directly. This also creates a reusable service layer for future integrations (webhooks, background jobs). Migrate one entity at a time, starting with the highest-traffic tools.
Two functions query the same system settings data independently:
getEnabledMCPTools() — called on every tools/list requestisToolEnabled() — called on every single tool callBoth call getSystemSettingsInternal(workspaceDb) which queries MongoDB. In a typical LLM session with 20 tool calls, this results in 20+ redundant DB queries for data that rarely changes.
Cache the enabled tools list once per session with a 30–60 second TTL. If an admin changes the setting mid-session, it picks up within one TTL window.
20 tool calls = 21 DB queries
(1 list + 20 calls)
20 tool calls = 1 DB query
(cached for 30s)
The CallToolRequestSchema handler catches thrown errors and marks them with isError: true. However, some tools return error objects instead of throwing. These reach the LLM as normal "successful" results, so the LLM may not realize the operation failed and won't retry or inform the user.
| Pattern | Code | Detected as Error? | Used In |
|---|---|---|---|
| throw Error | throw new Error('Not found') |
Yes — caught by server.ts | create_task, get_*_details |
| return { error } | return { error: 'Invalid ID' } |
No — treated as success | create_opportunity |
| return { success: false } | return { success: false, error: '...' } |
No — treated as success | invalidObjectIdError() |
Replace all return { error: ... } and return { success: false, ... } with throw new Error(...). The catch block in server.ts:119 already handles this correctly.
When a Claude Desktop user closes their laptop, loses connection, or their browser crashes, the MCP session stays in server memory permanently. Over days/weeks of production use, these orphaned sessions accumulate and consume memory. The server only recovers on restart.
res.on('close') + heartbeat — works correctlyServer instance + Transport instance + closures with workspace referencesUsers interacting via Claude Desktop or other MCP clients experience inconsistent behavior: deleting a task requires a double-confirmation flow (two tool calls), while deleting a project, invoice, or contact happens instantly. There is no clear rule for which entities are "protected" and which aren't, making the system feel arbitrary.
| Tool | Confirmation? | Pattern |
|---|---|---|
delete_task | Yes | confirmed param + warning message |
delete_dispute | Yes | confirmed param + warning message |
delete_project | No | Immediate delete |
delete_job | No | Immediate delete |
delete_file | No | Immediate delete |
delete_interaction | No | Immediate delete |
delete_kb_article | No | Immediate delete |
delete_opportunity | No | Immediate delete |
delete_contact | No | Immediate delete |
delete_invoice | No | Immediate delete |
Apply the confirmed pattern to every delete tool. Maximum safety but adds one extra round-trip per delete.
Remove the confirmed pattern entirely. LLMs already ask users before destructive operations. Reduces tool call complexity.
LLM clients (Claude, GPT) are trained to ask users before making destructive calls. The confirmed parameter forces an extra tool call round-trip that slows the conversation without adding real safety — the LLM already confirms with the user. Removing it gives users a faster, more consistent experience.
Every tool in the system uses the api() wrapper to make REST calls. The get_workspace_language tool is the sole exception — it directly imports Mongoose models and queries the database.
extractValidObjectId() helper, creating a second place to maintainThe MCP tools log every API call, completion, search filter, and first result using console.log with a [MCP Tools] prefix. In production, these mix with all other stdout output with no way to filter by severity, tool name, or session.
args object (contact names, emails, phone numbers) logged to stdout, potentially captured by log aggregatorsRemove the "API call completed" log entirely — it adds noise without debugging value.
Tool handlers use TypeScript as { ... } casts (compile-time only, zero runtime protection) and forward values directly to the API. LLMs occasionally hallucinate wrong types, generate oversized strings, or pass malformed data. Without MCP-layer validation, these errors surface as cryptic API failures that the LLM can't self-correct from.
| Scenario | What Happens | Current Protection |
|---|---|---|
| LLM passes number where string expected | API may crash or store wrong type | None |
| 100KB description string | Stored in DB, inflates responses | None |
| Invalid email format | Stored as-is, breaks email sending later | None |
| Negative limit/offset | Unexpected query behavior | None |
| Invalid date string | API may crash or ignore filter | None |
Note: The API layer has its own validation, so data corruption risk is low. The real impact is user experience — API-level errors return generic messages that the LLM can't act on, while MCP-level validation can tell the LLM exactly what to fix (e.g., "limit must be between 1 and 100").
Replace inline as { ... } casts with Zod schemas. These provide runtime validation, clear error messages, and can also generate the tool's inputSchema (DRY).
When an LLM asks for "next 10 contacts" by passing offset: 10, the tool still sends page: 1 to the API. The result: the user always sees the same first page of results regardless of the offset they requested. Pagination through MCP tools is effectively broken for several entities.
| Tool | Accepts | Sends to API | Correct? |
|---|---|---|---|
search_contacts | offset, limit | offset + page: 1 | Ambiguous |
search_opportunities | offset, limit | offset + page: 1 | Ambiguous |
get_tasks_overview | offset, limit | page: Math.floor(offset/limit)+1 | Correct |
search_calls | offset, limit, page | all three | Over-specified |
If the API uses page-based pagination, compute page from offset and limit. The get_tasks_overview handler already does this correctly — apply the same pattern everywhere.
| # | Проблема | Приоритет | Трудозатраты | Влияние на проект | |
|---|---|---|---|---|---|
| 01 | Разделить монолит tools.ts (10K строк) | Высокий | Средние | Изоляция сбоев | → |
| 02 | Устранить HTTP-оверхед при вызовах к себе | Высокий | Высокие | Скорость ответа ИИ | → |
| 03 | Кэшировать системные настройки на сессию | Высокий | Низкие | Снижение нагрузки на БД | → |
| 04 | Исправить непоследовательную обработку ошибок | Высокий | Низкие | Баг в продакшне | → |
| 05 | Утечка памяти в Streamable HTTP сессиях | Средний | Низкие | Стабильность сервера | → |
| 06 | Непоследовательный паттерн подтверждения удаления | Средний | Низкие | Пользовательский опыт | → |
| 07 | get_workspace_language нарушает архитектуру | Средний | Низкие | Риск скрытых поломок | → |
| 08 | Заменить console.log структурированным логированием | Низкий | Низкие | Риск утечки ПД | → |
| 09 | Добавить валидацию ввода помимо ObjectId | Низкий | Средние | Восстановление ИИ после ошибок | → |
| 10 | Исправить несоответствие пагинации offset vs page | Низкий | Низкие | Сломанная функция | → |
Все определения и обработчики инструментов находятся в одном файле за единственной функцией handleMCPToolCall. Это создаёт конкретные риски для проекта:
handleMCPToolCall, выводя из строя все 190 инструментов разомОбработчики инструментов вызывают fetch('http://localhost:3000/api/...') для каждой операции — полный HTTP round-trip к функции в том же процессе. Два конкретных последствия для проекта:
get_contact_details (4 параллельных fetch) накапливают это| Сценарий | HTTP-оверхед | Вызовов | Потеряно |
|---|---|---|---|
| Простой поиск | ~3-5 мс | 1 | ~5 мс |
get_contact_details | ~3-5 мс × 4 | 4 параллельных | ~15 мс |
| Создание счёта (авто-товары) | ~3-5 мс × N | 1 + N товаров | ~25 мс |
| LLM-сессия (20 вызовов) | ~3-5 мс × 20+ | 20+ | ~80 мс |
Выделить логику контроллеров в импортируемые сервисные функции, которые API-маршруты и MCP-инструменты смогут вызывать напрямую. Это также создаёт переиспользуемый сервисный слой для будущих интеграций (вебхуки, фоновые задачи). Мигрировать по одной сущности, начиная с самых частых инструментов.
Две функции независимо запрашивают одни и те же системные настройки:
getEnabledMCPTools() — вызывается при каждом запросе tools/listisToolEnabled() — вызывается при каждом вызове инструментаОбе вызывают getSystemSettingsInternal(workspaceDb), которая обращается к MongoDB. В типичной LLM-сессии с 20 вызовами это приводит к 20+ избыточным запросам к БД для данных, которые редко меняются.
Кэшировать список включённых инструментов один раз на сессию с TTL 30–60 секунд. Если админ изменит настройку во время сессии, она подхватится в пределах одного TTL-окна.
20 вызовов = 21 запрос к БД
(1 список + 20 вызовов)
20 вызовов = 1 запрос к БД
(кэш на 30 сек)
Обработчик CallToolRequestSchema ловит выброшенные ошибки и помечает их isError: true. Однако некоторые инструменты возвращают объекты ошибок вместо выброса. Они доходят до LLM как обычные «успешные» результаты, и LLM может не понять, что операция провалилась, не повторит попытку и не сообщит пользователю.
| Паттерн | Код | Определяется как ошибка? | Используется в |
|---|---|---|---|
| throw Error | throw new Error('Not found') |
Да — ловится в server.ts | create_task, get_*_details |
| return { error } | return { error: 'Invalid ID' } |
Нет — считается успехом | create_opportunity |
| return { success: false } | return { success: false, error: '...' } |
Нет — считается успехом | invalidObjectIdError() |
Заменить все return { error: ... } и return { success: false, ... } на throw new Error(...). Блок catch в server.ts:119 уже обрабатывает это корректно.
Когда пользователь Claude Desktop закрывает ноутбук, теряет соединение или его браузер падает, MCP-сессия навсегда остаётся в памяти сервера. За дни/недели работы эти осиротевшие сессии накапливаются и потребляют память. Сервер восстанавливается только при перезапуске.
res.on('close') + heartbeat — работает корректноServer + Transport + замыкания со ссылками на workspaceПользователи, взаимодействующие через Claude Desktop или другие MCP-клиенты, сталкиваются с непоследовательным поведением: удаление задачи требует двойного подтверждения (два вызова инструмента), тогда как удаление проекта, счёта или контакта происходит мгновенно. Нет чёткого правила, какие сущности «защищены», а какие нет.
| Инструмент | Подтверждение? | Паттерн |
|---|---|---|
delete_task | Да | параметр confirmed + предупреждение |
delete_dispute | Да | параметр confirmed + предупреждение |
delete_project | Нет | Немедленное удаление |
delete_job | Нет | Немедленное удаление |
delete_file | Нет | Немедленное удаление |
delete_interaction | Нет | Немедленное удаление |
delete_kb_article | Нет | Немедленное удаление |
delete_opportunity | Нет | Немедленное удаление |
delete_contact | Нет | Немедленное удаление |
delete_invoice | Нет | Немедленное удаление |
Применить паттерн confirmed ко всем инструментам удаления. Максимальная безопасность, но добавляет один дополнительный round-trip на удаление.
Убрать паттерн confirmed полностью. LLM уже спрашивают пользователей перед деструктивными операциями. Упрощает логику инструментов.
LLM-клиенты (Claude, GPT) обучены спрашивать пользователя перед деструктивными вызовами. Параметр confirmed вынуждает делать дополнительный вызов, который замедляет диалог без реальной безопасности — LLM и так подтверждает с пользователем. Убрав его, пользователи получат более быстрый и последовательный опыт.
Каждый инструмент в системе использует обёртку api() для REST-вызовов. Инструмент get_workspace_language — единственное исключение: он напрямую импортирует Mongoose-модели и делает запрос к базе данных.
extractValidObjectId(), создавая второе место для поддержкиMCP-инструменты логируют каждый API-вызов, завершение, поисковый фильтр и первый результат через console.log с префиксом [MCP Tools]. В продакшне они смешиваются со всем остальным stdout-выводом без возможности фильтрации по severity, имени инструмента или сессии.
args (имена, email, телефоны) логируется в stdout, потенциально попадая в агрегаторы логовУбрать лог «API call completed» полностью — он создаёт шум без ценности для отладки.
Обработчики инструментов используют TypeScript as { ... } приведения (только на этапе компиляции, ноль защиты в рантайме) и передают значения напрямую в API. LLM иногда галлюцинируют неправильные типы, генерируют слишком длинные строки или передают некорректные данные. Без валидации на уровне MCP эти ошибки проявляются как непонятные сбои API, из которых LLM не может самостоятельно восстановиться.
| Сценарий | Что происходит | Текущая защита |
|---|---|---|
| LLM передаёт число вместо строки | API может упасть или сохранить неверный тип | Нет |
| Описание на 100КБ | Сохраняется в БД, раздувает ответы | Нет |
| Невалидный формат email | Сохраняется как есть, ломает отправку email позже | Нет |
| Отрицательный limit/offset | Неожиданное поведение запроса | Нет |
| Невалидная строка даты | API может упасть или проигнорировать фильтр | Нет |
Примечание: Уровень API имеет собственную валидацию, поэтому риск повреждения данных низок. Реальное влияние — на UX: ошибки уровня API возвращают общие сообщения, с которыми LLM не может работать, тогда как валидация MCP может сказать LLM точно, что исправить (напр., «limit должен быть от 1 до 100»).
Заменить inline as { ... } приведения на Zod-схемы. Они обеспечивают runtime-валидацию, понятные сообщения об ошибках и могут генерировать inputSchema инструмента (DRY).
Когда LLM запрашивает «следующие 10 контактов» передавая offset: 10, инструмент всё равно отправляет page: 1 в API. Результат: пользователь всегда видит одну и ту же первую страницу вне зависимости от запрошенного offset. Пагинация через MCP-инструменты фактически сломана для нескольких сущностей.
| Инструмент | Принимает | Отправляет в API | Корректно? |
|---|---|---|---|
search_contacts | offset, limit | offset + page: 1 | Неоднозначно |
search_opportunities | offset, limit | offset + page: 1 | Неоднозначно |
get_tasks_overview | offset, limit | page: Math.floor(offset/limit)+1 | Корректно |
search_calls | offset, limit, page | all three | Избыточно |
Если API использует постраничную пагинацию, вычислять page из offset и limit. Обработчик get_tasks_overview уже делает это корректно — применить тот же паттерн повсюду.