fix(mcp): tool allowlist stored/read as jsonb string, not array (edit-page crash + allowlist not enforced) #172

Merged
Ghost merged 1 commits from fix/mcp-tool-allowlist-jsonb-shape into develop 2026-06-24 17:14:57 +03:00

Closes the MCP-server-edit crash (сообщил в TG: TypeError: Ke.map is not a function, началось после сохранения вайтлиста инструментов).

Симптом

Открытие формы редактирования MCP-сервера, у которого сохранён tool allowlist, роняло всю страницу настроек. Хуже того — сам allowlist при этом молча не применялся.

Первопричина (одна на оба бага)

Колонка tool_allowlist (jsonb) round-trip'ится как JSON-строка, а не массив. jsonbArray биндил JSON.stringify(value) (уже JSON-строку) прямо в ::jsonb-каст; node-postgres определяет тип параметра как jsonb и JSON-стрингифит его второй раз → в колонке лежит jsonb string scalar ("[\"a\"]", jsonb_typeof = string), а не массив. На чтении драйвер отдаёт JS-строку '["a"]'. Дальше:

  • TagsInput в форме делает .map по строке → краш страницы;
  • mcp-clients делает Array.isArray(allow) → для строки false → проваливается в «без ограничений» и отдаёт агенту все инструменты сервера.

Фикс (оба проверены на стенде)

  • Write: jsonbArray кастует ::text::jsonb — параметр биндится как text (шлётся как есть) и парсится в настоящий jsonb-массив. Новые строки: jsonb_typeof=array.
  • Read: normalizeRow прогоняет каждую прочитанную строку через parseToolAllowliststring[] | null для обеих форм (массив проходит как есть; JSON-строка парсится; null/мусор → null). Это чинит уже испорченные строки на чтении, без миграции данных. Применено в findById/listByWorkspace/listEnabled.
  • Client: защитный Array.isArray(...) ? ... : [] в форме — кривая форма больше не уронит настройки.

Проверка на стенде

  • Старая double-encoded строка теперь читается как массив ['alpha','beta'].
  • Новый сервер с allowlist сохраняется как jsonb_typeof=array (["gamma","delta"]).
  • Тестовые серверы за собой подчистил.

Тесты

ai-mcp-server.repo.spec — 8 кейсов на parseToolAllowlist (массив, JSON-строка-чтение, null, пусто, не-массив json, непарсимое, не-строковые элементы, не-строковый примитив). mcp-servers-to-view + mcp-namespacing — зелёные. client+server tsc чисто.

🤖 Generated with Claude Code

Closes the MCP-server-edit crash (сообщил в TG: `TypeError: Ke.map is not a function`, началось после сохранения вайтлиста инструментов). ## Симптом Открытие формы редактирования MCP-сервера, у которого сохранён tool allowlist, роняло всю страницу настроек. Хуже того — сам allowlist при этом **молча не применялся**. ## Первопричина (одна на оба бага) Колонка `tool_allowlist` (jsonb) round-trip'ится как JSON-**строка**, а не массив. `jsonbArray` биндил `JSON.stringify(value)` (уже JSON-строку) прямо в `::jsonb`-каст; node-postgres определяет тип параметра как jsonb и JSON-стрингифит его **второй раз** → в колонке лежит jsonb **string scalar** (`"[\"a\"]"`, `jsonb_typeof = string`), а не массив. На чтении драйвер отдаёт JS-строку `'["a"]'`. Дальше: - TagsInput в форме делает `.map` по строке → краш страницы; - `mcp-clients` делает `Array.isArray(allow)` → для строки `false` → проваливается в «без ограничений» и отдаёт агенту **все** инструменты сервера. ## Фикс (оба проверены на стенде) - **Write:** `jsonbArray` кастует `::text::jsonb` — параметр биндится как text (шлётся как есть) и парсится в настоящий jsonb-массив. Новые строки: `jsonb_typeof=array`. - **Read:** `normalizeRow` прогоняет каждую прочитанную строку через `parseToolAllowlist` → `string[] | null` для обеих форм (массив проходит как есть; JSON-строка парсится; null/мусор → null). Это **чинит уже испорченные строки на чтении**, без миграции данных. Применено в `findById`/`listByWorkspace`/`listEnabled`. - **Client:** защитный `Array.isArray(...) ? ... : []` в форме — кривая форма больше не уронит настройки. ## Проверка на стенде - Старая double-encoded строка теперь читается как массив `['alpha','beta']`. - Новый сервер с allowlist сохраняется как `jsonb_typeof=array` (`["gamma","delta"]`). - Тестовые серверы за собой подчистил. ## Тесты `ai-mcp-server.repo.spec` — 8 кейсов на `parseToolAllowlist` (массив, JSON-строка-чтение, null, пусто, не-массив json, непарсимое, не-строковые элементы, не-строковый примитив). `mcp-servers-to-view` + `mcp-namespacing` — зелёные. client+server tsc чисто. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Ghost added 1 commit 2026-06-24 17:12:24 +03:00
Opening the edit form for an MCP server that has a saved tool allowlist crashed
the whole settings page (`TypeError: Ke.map is not a function` in Mantine) — and,
worse, the allowlist was silently NOT enforced. Both stem from one root cause:
the `tool_allowlist` jsonb column round-trips as a JSON STRING, not an array.

Root cause: `jsonbArray` bound `JSON.stringify(value)` (already a JSON string)
straight to a `::jsonb` cast. node-postgres infers the param type as jsonb and
JSON-stringifies it a SECOND time, so the column stored a jsonb STRING SCALAR
(`"[\"a\"]"`, jsonb_typeof = string) instead of an array. On read the driver
hands back the JS string `'["a"]'`. Then:
  - the edit form's TagsInput called `.map` on a string -> page crash;
  - mcp-clients did `Array.isArray(allow)` -> false for a string -> fell through
    to "no restriction" and exposed ALL of the server's tools.

Fix (both verified on the stand):
- Write: `jsonbArray` casts `::text::jsonb` so the param is bound as text (sent
  verbatim) and parsed into a real jsonb array. New rows now store
  jsonb_typeof=array.
- Read: `normalizeRow` runs every fetched row through `parseToolAllowlist`, which
  returns `string[] | null` for both shapes (already-array passes through; a JSON
  string is parsed; null/invalid -> null). This REPAIRS existing double-encoded
  rows on read, so the UI and the allowlist enforcement work without a data
  migration. Applied in findById / listByWorkspace / listEnabled.
- Client: defensive `Array.isArray(...) ? ... : []` guard in the form so a bad
  shape can never take the settings page down again.

Tests: ai-mcp-server.repo.spec (8 cases for parseToolAllowlist — array, the
JSON-string read, null, empty, non-array json, unparseable, non-string elements,
non-string primitive). mcp-servers-to-view + mcp-namespacing still green.
Verified live: an old double-encoded row now reads as an array; a newly created
server stores jsonb_typeof=array.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ghost merged commit 04a418e1a6 into develop 2026-06-24 17:14:57 +03:00
Ghost deleted branch fix/mcp-tool-allowlist-jsonb-shape 2026-06-24 17:14:57 +03:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#172