ci: zod MCP schema gate + codex-desktop release reminder#2
Conversation
Guard against regressions of #1 (boolean outputSchema nodes that strict MCP clients reject) by validating tools/list with the actual @modelcontextprotocol/sdk zod schema: - scripts/zod-check: self-contained Node check that parses tools/list with ListToolsResultSchema, the same validation mcphub/Claude Desktop run. CI-only; not part of the published npm package. - zod-schema job: pre-release gate against the locally built binary, added to release-binary's needs so a broken schema can never publish. - validate-published job: post-publish, installs the published npm package and re-runs the zod check against it. Also add notify-codex-desktop: on a release tag, open a reminder issue in this repo to sync codex-desktop-linux with the release content.
There was a problem hiding this comment.
Code Review
This pull request introduces a regression guard for MCP tool list responses by adding a Node.js script that validates server output against Zod schemas. The script performs an initialization handshake and parses the tool list to ensure compatibility with strict clients. Review feedback suggested improving the robustness of the script by refining command-line argument parsing to handle missing values and ensuring safe UTF-8 processing of the server's stdout stream by explicitly setting the encoding.
| function parseArgs(argv) { | ||
| const args = { command: 'target/debug/computer-use-linux' }; | ||
| for (let i = 0; i < argv.length; i += 1) { | ||
| if (argv[i] === '--command') { | ||
| args.command = argv[i + 1]; | ||
| i += 1; | ||
| } | ||
| } | ||
| return args; | ||
| } |
There was a problem hiding this comment.
The parseArgs function does not handle cases where --command is provided without a following value (e.g., as the last argument). This results in args.command being set to undefined, which causes spawn to throw a TypeError synchronously. Adding a check for the existence of the next argument makes the script more robust.
function parseArgs(argv) {
const args = { command: 'target/debug/computer-use-linux' };
for (let i = 0; i < argv.length; i += 1) {
if (argv[i] === '--command' && argv[i + 1]) {
args.command = argv[i + 1];
i += 1;
}
}
return args;
}|
|
||
| const { command } = parseArgs(process.argv.slice(2)); | ||
|
|
||
| const child = spawn(command, ['mcp'], { stdio: ['pipe', 'pipe', 'inherit'] }); |
There was a problem hiding this comment.
It is safer to set the encoding on the stdout stream to 'utf8'. This ensures that the 'data' event emits strings and correctly handles multi-byte UTF-8 characters that might be split across chunk boundaries. Using chunk.toString('utf8') on raw buffers can occasionally lead to corrupted characters if the split happens in the middle of a multi-byte sequence.
const child = spawn(command, ['mcp'], { stdio: ['pipe', 'pipe', 'inherit'] });
child.stdout.setEncoding('utf8');| child.stdout.on('data', (chunk) => { | ||
| buffer += chunk.toString('utf8'); |
- Use `npm ci --ignore-scripts` for the MCP SDK install in both the pre-release gate and the post-publish job: reproducible from the committed lockfile and no third-party install scripts run. The published-package install keeps scripts so its postinstall can fetch the release binary. - check.mjs: don't fall through after reporting a ZodError.
- parseArgs: ignore a trailing --command with no value instead of setting command to undefined (would throw in spawn). - set stdout encoding to utf8 and append chunks directly, so multi-byte characters split across stream chunks are decoded correctly.
|
Addressed the gemini-code-assist review in 0f4aec9's follow-up:
CodeQL flagged |
CodeQL flagged dtolnay/rust-toolchain@stable and Swatinem/rust-cache@v2 as unpinned non-immutable actions. Pin both to commit SHAs repo-wide (version kept in a trailing comment) so the whole workflow is consistent, not just the new zod-schema job. dtolnay/rust-toolchain's `toolchain` input defaults to `stable`, so dropping the @stable ref keeps the stable channel; no job overrides it.
Why
Follow-up to #1. The boolean
outputSchemaregression was only catchable by the in-housemcp_safety_check.pyheuristics, not by the actual validation strict clients run. This adds a real zod gate using@modelcontextprotocol/sdkso the exact failure can never ship again, plus a release-time reminder to keepcodex-desktop-linuxin sync.What
zod schema validation (both pre- and post-release)
scripts/zod-check/— self-contained Node ESM check that does the MCPinitializehandshake, callstools/list, and parses the result withListToolsResultSchema(the sameAssertObjectSchemapath that rejectedreceived: truein Bug:outputSchema.properties.receivedistrue(boolean) causing Zod validation failure in MCP clients #1). CI-only; excluded from the published npmfiles.zod-schemajob — pre-release gate against the locally built binary; added torelease-binary.needs, so a broken schema blocks publish.validate-publishedjob — runs afterpublish-npm, installs the published@agent-sh/computer-use-linux@<version>and re-runs the zod check against it.release reminder
notify-codex-desktopjob — on av*tag, opens an issue in this repo reminding to sync codex-desktop-linux with the release content (extracts the matching CHANGELOG section). Uses the defaultGITHUB_TOKEN(issues: write); dedupes by title.Verification (local)
node scripts/zod-check/check.mjsagainst the v0.2.2 binary →OK: 16 tools validated.ListToolsResultSchemaa{ properties: { received: true } }node throwsZodErrorwith the path pointing atreceived— i.e. the guard catches the exact Bug:outputSchema.properties.receivedistrue(boolean) causing Zod validation failure in MCP clients #1 shape.ci.ymlparses; 15 jobs.