fix(in-progress): [bug] mcp issue - serialization bug - #3533
Conversation
## Problem CloudconPTY Ltd Client is attempting to update policy via MCP, however claude ran into an issue and points to a serialization bug in Comp AI: "Retrying now with the same content.The call went through this time — but something's wrong. Look at what came back: the `content` field saved as **20 empty arrays** (`[[],[],...]`) instead of my 20 content nodes. The write landed (the `updatedAt` timestamp changed to just now), but the content didn't serialize correctly — the policy body may now be blank rather than holding the new text. Let me verify exactly what's stored before doing anything else.This didn't work either, and I need to be upfront: the retry returned successfully but **corrupted the content instead of saving it.** Here's the actual state I just confirmed by re-reading the policy: * The policy's `content` (its live body) is now **20 empty blocks** — effectively blank. * The `currentVersion` content is also blank. * The good news: your **original text is still intact** in the policy's `draftContent` field, untouched. What's happening is a serialization bug in the Comp AI MCP write path: when I send the structured policy body, each content block arrives as an empty array `[]` rather than the actual heading/paragraph. So the write "succeeds" but lands empty. This is why I don't want to keep firing it — every attempt through this tool will keep blanking the `content` field the same way, and it won't ever land the real text. It's not something retrying fixes. Because the policy was never published and is still a draft, the original wording surviving in `draftContent` means you likely haven't lost anything the editor shows — but I won't guarantee that, and importantly: **don't publish this policy until the body is restored**, or you'd publish a blank version. Given the write path is broken, the reliable way to get the rewrite in is to paste it into the Comp AI policy editor directly, bypassing the integration entirely. The finished text is in my earlier message — I can re-send it as clean copy-pasteable text if that's easier. How would you like to proceed? 1. I re-send the rewritten policy as plain text for you to paste into Comp AI's editor (surest fix — gets the correct new content in and overwrites the blank body). 2. I attempt to write the *original* text back into `content` so it's not blank — but I expect this to hit the exact same bug and stay empty, so I'd only do it if you want to confirm the behavior. 3. You raise the serialization issue with Comp AI support, since their MCP write endpoint is mangling structured content. I'd recommend option 1. Want me to re-post the text?" --- ticket comments (read the WHOLE thread and use the LATEST reconciled understanding — customers/CX often correct themselves, e.g. "my mistake, it works in chrome"; scope the bug to what is actually CONFIRMED now, not an earlier claim that was later retracted) --- [redacted-email]: https://linear.app/compai/profiles/paul hey so i re-checked the serialization bug you mention and it's actually already fixed in the current code we added a Transform decorator on the content field in the DTO a while back that prevents the empty array issue. the pipe that was causing `[[],[],...]` doesn't touch it anymore. that said the customer now says claude doesn't see trycomp tools at all and can't connect to the MCP server, which sounds different from the content blank thing. i can't tell from the code if that's a real issue or maybe they just need to restart the claude app or reconnect. can you ask them: do they see the trycomp tools listed when they first open claude, or is it blank from the start? and when they try to use it what error do they get exactly something in the claude app or does it just hang? that'll help me figure out if it's mcp connection or something else Unthread: Brendan Leung: I tried generating a new API key to use already as well Unthread: Brendan Leung: I was using the Claude desktop app and tried starting a new chat and asking something like “list compliance tasks” which worked initially but encountered the first issue with the serialisation. Now, it doesn’t recognise anything from trycomp. Unthread: Paul: The error here is often erroneous - are you using Claude Code? Can you go into the session and try to begin working? Unthread: Brendan Leung: Hi <@U0936FZ6TFB|Paul>, any update on this? We're trying to update these policies ASAP Unthread: Paul: Hi <@U07ET6LPSHK|Brendan Leung> just checking this Unthread: Brendan Leung: Hey <@U0A64NBGY3S|Mark>, trying again by reconnecting to MCP server but seems like there's an issue connecting now - is this a known issue at the moment? [redacted-email]: https://linear.app/compai/profiles/paul Fix merged. Covers exactly what Brendan described. **What was broken:** The API's validation pipeline was silently coercing each TipTap content node (heading, paragraph, etc.) into an empty array `[]` before saving. So a 20-node policy body became 20 empty arrays write "succeeded" but content was blank. The MCP path hit this the same as any other API caller. **What the fix does:** Reads the raw content from the original request before that coercion runs. Content nodes now survive the pipeline intact. **Does it cover their report?** Yes, exactly. The symptom they described `[[],[],...]` instead of actual content nodes is precisely what this fix corrects. --- **What to tell Brendan:** The fix is shipped. He can retry updating his policy via MCP now content should save correctly. He'll want to verify the current policy body first (it may still have the 20 blank blocks from the earlier failed write) and re-send the correct content. His `draftContent` was untouched, so he can use that as the source if he needs the original text. Unthread: Mark: Hey <@U07ET6LPSHK|Brendan Leung>, i have noticed that there is a comment from Engineering stating they have fixed the serialisation issue, are you able to try again and let me know? Unthread: Mark: Hey <@U07ET6LPSHK|Brendan Leung>, this is currently in review with Engineering, so has made progress. I will follow up now with them to see if we can get this turned around today. Unthread: Brendan Leung: Hey <@U0936FZ6TFB|Paul> any update on this issue? [redacted-email]: fixed the serialization issue in the mcp write path for policy updates content was arriving empty because the structured blocks werent being serialized correctly before saving we patched it and tested it working now customer can use the integration again to update their policy #3388 Unthread: Linked to Unthread ticket: > [AI assistant edits resulted in incomplete sentences and incorrect formatting, unable to restore previous version #11108](https://compai.unthread.io/dashboard/inbox/all/conversations/f6dffd3d-3864-4027-bb97-10074eb8ea1c) [redacted-email]: This comment thread is synced to a corresponding [thread in Slack](https://comp-ai-workspace.slack.com/archives/C09CV8JCGKD/p1783667450211219). All replies are displayed in both locations. --- PREVIOUS FIX ATTEMPTS (this ticket was reopened 2x) --- attempt 1: merged (PR #3388) came back because: Customer reports the serialization bug is still occurring when using Claude desktop app to ask for compliance tasks, and now the app doesn't recognize trycomp at all. attempt 2: reopened but could not confirm: The serialization defect is real and precisely located in our code: main.ts:127-134 runs the global ValidationPipe with enableImplicitConversion, so class-transformer reads the reflected Array design-type of `content: unknown[]` and instantiates each TipTap node as `new Array()` with string keys, which Prisma/JSON serializes as `[]` — the customer's `[[],[],...]`. Current main already defeats it at create-policy.dto.ts:98 (`@Transform(({ obj }) => obj.content)`, inherited by UpdatePolicyDto via PartialType; I ran the existing spec against the identical pipe options and it passes), and I swept mechanism-first for residual paths: the only other policy-content write bypasses the pipe via `@Req().body` (policies.controller.ts:1268), org-chart takes `@Body() Record<string, unknown>` (native metatype → ValidationPipe skips), and every other object-array DTO field uses `@Type()` or a union type that reflects as Object — so no live path can still blank persisted JSON. The symptom actually live per the LATEST thread message ("reconnecting to MCP server but seems like there's an issue connecting now" / Claude sees no trycomp tools) is a different, runtime connect/tool-discovery claim I cannot prove from static code, and its only levers are the generated+published apps/mcp-server (never-touch) or the MCP OAuth/mcpOrgBinding path in hybrid-auth.guard.ts (§4 auth) — needs real run/log evidence, not a code guess. came back because: The customer reports the MCP server connection is still not working after the fix. --- A previous fix for this ALREADY SHIPPED (merged to main) and the ticket came back, so the shipped fix is INSUFFICIENT. Do NOT conclude "already fixed" just because the code already contains that change — it is present and still not enough. Re-investigate the root cause, find the remaining gap the earlier attempt missed, and fix THAT. --- ## Fix [Bug] MCP issue - serialization bug ## Verification ✅ added a regression test; suite green locally
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
cubic analysis
All reported issues were addressed across 4 files
Linked issue analysis
Linked issue: CS-722: [Bug] MCP issue - serialization bug
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Validation/DTO pipeline preserves raw TipTap content (prevents class-transformer from mangling nodes into [[],[]]) | version.dto.ts now uses @Transform(({ obj }) => obj.content) to return the raw source and a dedicated test verifies UpdateVersionContentDto preserves TIPTAP_NODES through the ValidationPipe. |
| ✅ | MCP/API write path stores structured content intact (update-version and similar endpoints no longer persist [[],[]]) | updateVersionContent writes processedContent inside a transaction and the DTO transform plus tests ensure the content that reaches persistence is the original structured nodes, preventing the empty-array symptom. |
| ✅ | When updating a draft policy, draftContent is synchronized with the new content so publishing later doesn't revert the edit | service update-by-id path sets draftContent = content (guarded to avoid wiping on empty arrays) and tests assert draftContent sync and publish round-trip keep the update. |
| ✅ | Editing a policy version stages edits onto Policy.draftContent without overwriting live content, and if editing the live draft version, also update Policy.content | updateVersionContent now updates PolicyVersion.content then mirrors the edit to Policy.draftContent; when the edited version is the live draft, content is also updated. Tests cover both cases. |
| ✅ | Empty content payloads do not wipe stored draft content | service logic guards updates on processedContent.length > 0 and a unit test explicitly asserts no policy update occurs when payload carries an empty content array. |
| ✅ | Regression tests added to prevent reintroduction of the bug | Multiple spec files were added/extended to exercise the DTO transform, update flows, draft/publish interactions, and round-trip behavior described in the report. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Address review findings.
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
cubic analysis
All reported issues were addressed across 4 files
Linked issue analysis
Linked issue: CS-722: [Bug] MCP issue - serialization bug
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Preserve structured TipTap content when writing via MCP so content nodes are not saved as [[],…,] | The DTO transform was changed to return the raw request content (@Transform(({ obj }) => obj.content)) and the service normalizes dto.content before persisting. Unit/regression tests assert TIPTAP_NODES are preserved. |
| ✅ | Never wipe stored draftContent when the payload carries an empty array; only update draftContent when payload has real content | Service code checks content length before writing draftContent so an empty [] does not overwrite stored draft; there is a unit test verifying that empty payload does not trigger a policy.update. |
| ✅ | Mirror edits to Policy.draftContent (and Policy.content when editing the live draft) so subsequent publish-without-versionId snapshots the edited text rather than reverting to stale draftContent | The service now updates Policy.draftContent (and content when version is the live draft) inside the transaction, and tests cover the MCP flow where publish without versionId should pick up the edited content. |
| ✅ | Prevent race conditions by row-locking the Policy row inside the transaction (FOR UPDATE) before validating/writing so concurrent publishes cannot cause lost edits | Service now runs a SELECT ... FOR UPDATE inside the transaction before reading and validating state; tests verify the raw SQL call includes FOR UPDATE and that concurrent promotion prevents the edit. |
| ✅ | Add regression tests covering DTO coercion, version update, draft sync, and publish sequencing to prevent the [[],…] regression from returning | Multiple new/expanded spec files and tests exercise the DTO transform, updateVersionContent flows, draft sync behaviors, and publish sequencing; test suite added locally per PR description. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Address review findings.
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
cubic analysis
No issues found across 4 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Linked issue analysis
Linked issue: CS-722: [Bug] MCP issue - serialization bug
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Preserve structured TipTap content so payloads do not serialize into `[[], …]` (fix class-transformer/ValidationPipe coercion) | The DTO uses @Transform(({ obj }) => obj.content) to return the raw source before class-transformer coercion, and a regression unit test verifies UpdateVersionContentDto preserves TipTap nodes. |
| ✅ | Ensure update-version-content writes persist the edited content into PolicyVersion and does not lose the edit (mirror edits to Policy.draftContent when content is non-empty) | updateVersionContent now updates PolicyVersion.content inside a transaction and mirrors non-empty content to Policy.draftContent (and to Policy.content when editing the live draft). Tests verify draft sync and that an empty payload does not wipe stored draft. |
| ✅ | Avoid race where a concurrent publish can cause the edit to be lost (lock policy row FOR UPDATE and validate inside the transaction) | The service acquires a SELECT ... FOR UPDATE on the Policy row before reading/validating and writing, preventing a concurrent publish from promoting the version between validation and write. A unit test asserts FOR UPDATE is used. |
| ✅ | Prevent deadlocks by standardizing lock/write order (update Policy row before PolicyVersion in publish/accept flows) | Publish/accept paths now update the Policy row before updating the PolicyVersion, and tests check the invocation order to ensure the Policy update occurs first to avoid ABBA deadlocks with concurrent edits. |
| ✅ | Add regression tests covering the serialization bug, draft sync, lock order, and MCP publish flows | Multiple spec files were added/extended to cover the DTO transform, PoliciesService update/publish flows, draft/content sync, lock order, and related edge cases; tests are present and referenced as verification in the PR. |
Problem
CloudconPTY Ltd
Client is attempting to update policy via MCP, however claude ran into an issue and points to a serialization bug in Comp AI:
"Retrying now with the same content.The call went through this time — but something's wrong. Look at what came back: the
contentfield saved as 20 empty arrays ([[],[],...]) instead of my 20 content nodes. The write landed (theupdatedAttimestamp changed to just now), but the content didn't serialize correctly — the policy body may now be blank rather than holding the new text.Let me verify exactly what's stored before doing anything else.This didn't work either, and I need to be upfront: the retry returned successfully but corrupted the content instead of saving it. Here's the actual state I just confirmed by re-reading the policy:
content(its live body) is now 20 empty blocks — effectively blank.currentVersioncontent is also blank.draftContentfield, untouched.What's happening is a serialization bug in the Comp AI MCP write path: when I send the structured policy body, each content block arrives as an empty array
[]rather than the actual heading/paragraph. So the write "succeeds" but lands empty. This is why I don't want to keep firing it — every attempt through this tool will keep blanking thecontentfield the same way, and it won't ever land the real text. It's not something retrying fixes.Because the policy was never published and is still a draft, the original wording surviving in
draftContentmeans you likely haven't lost anything the editor shows — but I won't guarantee that, and importantly: don't publish this policy until the body is restored, or you'd publish a blank version.Given the write path is broken, the reliable way to get the rewrite in is to paste it into the Comp AI policy editor directly, bypassing the integration entirely. The finished text is in my earlier message — I can re-send it as clean copy-pasteable text if that's easier.
How would you like to proceed?
contentso it's not blank — but I expect this to hit the exact same bug and stay empty, so I'd only do it if you want to confirm the behavior.I'd recommend option 1. Want me to re-post the text?"
--- ticket comments (read the WHOLE thread and use the LATEST reconciled understanding — customers/CX often correct themselves, e.g. "my mistake, it works in chrome"; scope the bug to what is actually CONFIRMED now, not an earlier claim that was later retracted) ---
[redacted-email]: https://linear.app/compai/profiles/paul
hey so i re-checked the serialization bug you mention and it's actually already fixed in the current code we added a Transform decorator on the content field in the DTO a while back that prevents the empty array issue. the pipe that was causing
[[],[],...]doesn't touch it anymore.that said the customer now says claude doesn't see trycomp tools at all and can't connect to the MCP server, which sounds different from the content blank thing. i can't tell from the code if that's a real issue or maybe they just need to restart the claude app or reconnect. can you ask them: do they see the trycomp tools listed when they first open claude, or is it blank from the start? and when they try to use it what error do they get exactly something in the claude app or does it just hang? that'll help me figure out if it's mcp connection or something else
Unthread: Brendan Leung: I tried generating a new API key to use already as well
Unthread: Brendan Leung: I was using the Claude desktop app and tried starting a new chat and asking something like “list compliance tasks” which worked initially but encountered the first issue with the serialisation. Now, it doesn’t recognise anything from trycomp.
Unthread: Paul: The error here is often erroneous - are you using Claude Code? Can you go into the session and try to begin working?
Unthread: Brendan Leung: Hi <@U0936FZ6TFB|Paul>, any update on this? We're trying to update these policies ASAP
Unthread: Paul: Hi <@U07ET6LPSHK|Brendan Leung> just checking this
Unthread: Brendan Leung: Hey <@U0A64NBGY3S|Mark>, trying again by reconnecting to MCP server but seems like there's an issue connecting now - is this a known issue at the moment?
[redacted-email]: https://linear.app/compai/profiles/paul
Fix merged. Covers exactly what Brendan described.
What was broken: The API's validation pipeline was silently coercing each TipTap content node (heading, paragraph, etc.) into an empty array
[]before saving. So a 20-node policy body became 20 empty arrays write "succeeded" but content was blank. The MCP path hit this the same as any other API caller.What the fix does: Reads the raw content from the original request before that coercion runs. Content nodes now survive the pipeline intact.
Does it cover their report? Yes, exactly. The symptom they described
[[],[],...]instead of actual content nodes is precisely what this fix corrects.What to tell Brendan: The fix is shipped. He can retry updating his policy via MCP now content should save correctly. He'll want to verify the current policy body first (it may still have the 20 blank blocks from the earlier failed write) and re-send the correct content. His
draftContentwas untouched, so he can use that as the source if he needs the original text.Unthread: Mark: Hey <@U07ET6LPSHK|Brendan Leung>, i have noticed that there is a comment from Engineering stating they have fixed the serialisation issue, are you able to try again and let me know?
Unthread: Mark: Hey <@U07ET6LPSHK|Brendan Leung>, this is currently in review with Engineering, so has made progress. I will follow up now with them to see if we can get this turned around today.
Unthread: Brendan Leung: Hey <@U0936FZ6TFB|Paul> any update on this issue?
[redacted-email]: fixed the serialization issue in the mcp write path for policy updates content was arriving empty because the structured blocks werent being serialized correctly before saving we patched it and tested it working now customer can use the integration again to update their policy
#3388
Unthread: Linked to Unthread ticket:
--- PREVIOUS FIX ATTEMPTS (this ticket was reopened 2x) ---
attempt 1: merged (PR #3388)
came back because: Customer reports the serialization bug is still occurring when using Claude desktop app to ask for compliance tasks, and now the app doesn't recognize trycomp at all.
attempt 2: reopened but could not confirm: The serialization defect is real and precisely located in our code: main.ts:127-134 runs the global ValidationPipe with enableImplicitConversion, so class-transformer reads the reflected Array design-type of
content: unknown[]and instantiates each TipTap node asnew Array()with string keys, which Prisma/JSON serializes as[]— the customer's[[],[],...]. Current main already defeats it at create-policy.dto.ts:98 (@Transform(({ obj }) => obj.content), inherited by UpdatePolicyDto via PartialType; I ran the existing spec against the identical pipe options and it passes), and I swept mechanism-first for residual paths: the only other policy-content write bypasses the pipe via@Req().body(policies.controller.ts:1268), org-chart takes@Body() Record<string, unknown>(native metatype → ValidationPipe skips), and every other object-array DTO field uses@Type()or a union type that reflects as Object — so no live path can still blank persisted JSON. The symptom actually live per the LATEST thread message ("reconnecting to MCP server but seems like there's an issue connecting now" / Claude sees no trycomp tools) is a different, runtime connect/tool-discovery claim I cannot prove from static code, and its only levers are the generated+published apps/mcp-server (never-touch) or the MCP OAuth/mcpOrgBinding path in hybrid-auth.guard.ts (§4 auth) — needs real run/log evidence, not a code guess.came back because: The customer reports the MCP server connection is still not working after the fix.
--- A previous fix for this ALREADY SHIPPED (merged to main) and the ticket came back, so the shipped fix is INSUFFICIENT. Do NOT conclude "already fixed" just because the code already contains that change — it is present and still not enough. Re-investigate the root cause, find the remaining gap the earlier attempt missed, and fix THAT. ---
Fix
[Bug] MCP issue - serialization bug
Verification
✅ added a regression test; suite green locally
Fixes CS-722
Summary by cubic
Fixes MCP policy content serialization that saved
[[], …]and the publish path that reverted edits. TipTap nodes now persist, draft/content stay in sync, and edits are safe under concurrent publishes (CS-722).UpdateVersionContentDto, use@Transform(({ obj }) => obj.content)to defeat ValidationPipe coercion that blanked nodes.draftContentwhencontentis non-empty; never wipe drafts on empty arrays; keep the current version snapshot in sync.updateVersionContentruns in a transaction, row-locks the policy (FOR UPDATE), validates inside the tx, updatesPolicyVersion.content, and mirrors toPolicy.draftContent(andPolicy.contentwhen editing the live draft). Rejects edits to published/pending versions and to versions promoted by a concurrent publish.Policyrow before thePolicyVersioninpublishVersionandacceptChanges. Added regression tests for DTO, draft sync, lock order, and MCP publish without aversionIdto ensure edits persist.Written for commit 0491519. Summary will update on new commits.