fix(mcp): tool allowlist stored/read as jsonb string, not array (edit-page crash + allowlist not enforced) #172
Reference in New Issue
Block a user
Delete Branch "fix/mcp-tool-allowlist-jsonb-shape"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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"]'. Дальше:.mapпо строке → краш страницы;mcp-clientsделаетArray.isArray(allow)→ для строкиfalse→ проваливается в «без ограничений» и отдаёт агенту все инструменты сервера.Фикс (оба проверены на стенде)
jsonbArrayкастует::text::jsonb— параметр биндится как text (шлётся как есть) и парсится в настоящий jsonb-массив. Новые строки:jsonb_typeof=array.normalizeRowпрогоняет каждую прочитанную строку черезparseToolAllowlist→string[] | nullдля обеих форм (массив проходит как есть; JSON-строка парсится; null/мусор → null). Это чинит уже испорченные строки на чтении, без миграции данных. Применено вfindById/listByWorkspace/listEnabled.Array.isArray(...) ? ... : []в форме — кривая форма больше не уронит настройки.Проверка на стенде
['alpha','beta'].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
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 referenced this pull request2026-06-25 12:00:53 +03:00