Skip to content

fix(mcp): keep padded optional arguments from filtering MCP reads - #1578

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-11352-drop-and-advertise-null-for-optional-mcp-arguments-to-stop
Oct 30, 2026
Merged

TheGreatAxios merged 2 commits into
mainfrom
cl-11352-drop-and-advertise-null-for-optional-mcp-arguments-to-stop

Conversation

@bwachman

Copy link
Copy Markdown
Contributor

Summary

  • Read-only MCP tools (server readOnlyHint, or a list_/get_-style name) no longer receive JSON null on optional arguments: the handler drops it, so a padded assignee: null does not filter Linear list_issues down to unassigned issues. Write tools keep null, which they use to clear a field.
  • On GPT-family providers, optional properties of MCP tools also admit null in the wire schema, so the model writes null for "not set" instead of a real value such as priority: 0. A property gains null only where the handler drops it again, and only when the widened property still validates null under JSON Schema.

Verification

  • bun run check (lint, typecheck, dead exports, build, test suite) passes: 9524 pass, 0 fail.
  • On codex/plus / gpt-5.6-terra, asking for every issue in release v0345-ef53e73667fa returns all 6 issues from one list_issues call in 5 of 5 runs; origin/main returns 0 in 5 of 5. Per-configuration counts are on CL-11352.
  • Linear's string assignee filter "null" returns the same unassigned issues as JSON null (raw MCP call), so "unassigned" stays reachable on read tools.

Fixes CL-11352

@bwachman

Copy link
Copy Markdown
Contributor Author

Self-Review (code-review skill)

The branch carries the following intentional exceptions, each authorized by Greybeard:

  • src/mcp/plugin.ts:118: on read-only MCP tools the handler drops JSON null from optional arguments for every provider, including a deliberate filter such as Linear's assignee: null ("unassigned"). The server's documented string form ("null") returns the same issues and is the supported way to ask for an unset field. Authorized by Greybeard on the grounds that the handler cannot tell GPT padding from a deliberate filter, and the documented string form keeps the filter reachable.
  • src/agent/tool-search.ts:566: tool_search result cards quote the server's original schema, while the GPT wire definition admits null on optional MCP properties. Authorized by Greybeard on the grounds that cards are lossy previews (a schema over 600 characters, such as Linear's list_issues, shows property names only) and the model calls against the promoted wire definition, which carries the null.

@linear-code

linear-code Bot commented Oct 29, 2026

Copy link
Copy Markdown

CL-11352

@corbits-triage

corbits-triage Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Triage: Pass

No changes needed from you. A maintainer will review this pull request.

TheGreatAxios

This comment was marked as duplicate.

@TheGreatAxios TheGreatAxios left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review · Approve

When a model fills an optional argument with null on a read-only tool, that null is removed before the call. A padded null no longer filters the results. On Linear, that is the difference between listing every issue in a release and getting none, because assignee: null means "unassigned." Write tools still receive null, so clearing a field still works. GPT models are told they may send null for "not set," and only on fields where that null will be removed again.

bun test src/mcp/placeholder-args.test.ts src/mcp/plugin.test.ts src/agent/tool-schema-normalize.test.ts src/session/assemble-runtime.test.ts src/mcp/tool-permissions.test.ts — 117 pass, 0 fail.

Two cases where the read-only check still keeps or drops the wrong null:

  • A list or get tool sometimes arrives marked "not destructive" or "can reach the network," but without an explicit read-only mark (src/mcp/tool-permissions.ts:61). We then treat it as a write and keep null. On Linear, assignee: null means "unassigned," so a request for every issue comes back empty. If the tool is also marked read-only, null is dropped and this does not happen.

  • Some tool names only look read-only (src/mcp/tool-permissions.ts:64). search_replace, get_or_create_issue, and similar names are treated as reads, so a null that was meant to clear a field is deleted and the field stays as it was. update_issue keeps null unless the server marks that tool read-only, in which case a clear is deleted too.

@TheGreatAxios
TheGreatAxios merged commit 5e1631f into main Oct 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants