Skip to content

fix: stop double JSON-encoding execute_sql and get_logs results - #339

Open
radmirnovii wants to merge 1 commit into
supabase:mainfrom
radmirnovii:fix/execute-sql-double-encoding
Open

fix: stop double JSON-encoding execute_sql and get_logs results#339
radmirnovii wants to merge 1 commit into
supabase:mainfrom
radmirnovii:fix/execute-sql-double-encoding

Conversation

@radmirnovii

@radmirnovii radmirnovii commented Jul 22, 2026

Copy link
Copy Markdown

Fixes #311

Root cause

As diagnosed in #311 (comment), execute_sql results were JSON-encoded twice:

  1. wrapWithUntrustedDataBoundary embeds the query rows via JSON.stringify inside the <untrusted-data-*> prose wrapper.
  2. createMcpServer's tool-call handler then serializes the whole output object ({ result: "<wrapper text>" }) with a second JSON.stringify to produce the text content.

A single backslash in queried data (e.g. the E'\\' literal in a PL/pgSQL function body) reached the model as 8 backslashes. Models routinely collapse that to the wrong count when writing the definition back, corrupting round-trips of function definitions. get_logs had the same double encoding.

Fix

Tools in @supabase/mcp-utils can now declare an optional textContent(output) serializer:

  • The returned text is sent verbatim as the text content (no second JSON.stringify).
  • The full output object is also returned as structuredContent, so typed clients keep parsing outputs exactly as before — the AI SDK integration (supabaseMcpToolSchemas) validates structuredContent first and never falls back to parsing the prose text.

execute_sql and get_logs use it to send the untrusted-data wrapper directly, so query data is now JSON-encoded exactly once. All other tools are unchanged.

This is also a first concrete step toward the outputSchema/structuredContent support mentioned in the issue: createMcpServer now emits structuredContent (currently only for tools that opt into textContent); advertising outputSchema in tools/list and emitting structuredContent universally can build on top of this.

Behavior change

  • execute_sql / get_logs text content is now the untrusted-data wrapper itself (prose containing single-encoded JSON) instead of a JSON object with the wrapper double-encoded inside. The text is meant for model consumption and was never parseable into row data in one step before.
  • Both tools' results now include structuredContent ({ result: string }).
  • tools/list output is untouched — no changes to tool names, descriptions, input schemas, or annotations (the frozen-field contract for ChatGPT snapshots is unaffected; outputSchema is not advertised in tools/list).
  • get_logs's internal output schema is corrected from { result: z.unknown() } to { result: z.string() } to match what it has always returned.

Verification

  • New regression test reproduces the exact scenario from the issue against PGlite: create a PL/pgSQL function containing E'\\', read it back via pg_get_functiondef through execute_sql, assert the text contains the E-string encoded exactly once (4 backslashes, not 8), re-create the function from the returned definition, and assert the stored body is byte-identical. Also asserts structuredContent matches the text.
  • New mcp-utils test covers textContent verbatim text + structuredContent emission.
pnpm --filter @supabase/mcp-utils test        # 13 passed (13)
pnpm --filter @supabase/mcp-server-supabase test:unit  # 192 passed (192)

Typecheck (tsc --noEmit) and biome check pass in both packages.

🤖 Generated with Claude Code

…base#311)

Results were JSON-encoded twice: once inside the untrusted-data wrapper
and again when the output object was serialized into MCP text content,
so backslashes in queried data reached the LLM quadrupled and corrupted
round-trips of function definitions.

Tools can now provide a `textContent` serializer whose text is sent
verbatim, with the full output also returned as `structuredContent` for
typed clients. execute_sql and get_logs use it, so results are encoded
exactly once. Includes a regression test for the E'\\' round-trip.
@radmirnovii
radmirnovii requested a review from a team as a code owner July 22, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

execute_sql corrupts backslash escaping in PL/pgSQL function bodies (round-trip failure)

1 participant