Treat zero-filled IDs in MCP tool arguments as omitted

What does this MR do and why?

GPT models in the Duo Agent Platform fill every optional MCP tool argument with a default, and some of those defaults break our tools. id: 0 next to a username makes get_user fail, zero IDs point lookups at records that can't exist, and empty old_str/new_str turn a plain add_commit create into a failed partial edit. This MR treats those values as omitted, the way we already treat null and "", and adds the dap_openai_non_strict_tools flag for the AI gateway change that stops GPT having to fill every argument in the first place (gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!7436 (merged)).

This is an alternative to !255139 (closed) by @terrichu, for #621520. That MR made optional fields nullable in our MCP tool schemas, which Gemini rejects. Here the fix moves to the AI gateway: it sends tools to OpenAI with strict mode off, so GPT no longer has to fill every argument (gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!7436 (merged)). This MR keeps the parts of !255139 (closed) that work the same for every model.

What changes
  • BaseService#reject_omitted_arguments now also drops zero and negative integers on ID fields (*_id, *_iid, and a bare id/iid), and 0 on integer fields with minimum: 1, such as first and page. A dropped value falls through to the normal "missing" validation.
  • add_commit only treats an action as a partial edit when old_str or new_str has content. Whitespace still counts, so "\n" is still a partial edit. An update with neither content nor a partial edit is refused, so it can't empty the file, and an empty content no longer blocks a partial edit.
  • dap_openai_non_strict_tools (gitlab_com_derisk, off by default) is pushed to the AI gateway with the other Duo Workflow flags. On its own it does nothing; ai-assist!7436 reads it.
  • Specs that pinned the old rejection of first: 0 and page: 0, and the zero work_item_iid create, now expect the value to be stripped.

save_merge_request keeps milestone_id: 0, because the REST API removes the milestone for 0. Tools can name the ID arguments where 0 is a value (arguments_where_zero_is_a_value); milestone_id is the only one today. With the gateway change, GPT can leave milestone_id out instead of sending 0. The tool description now says that 0 removes the milestone on update.

References

Related to #621520. Alternative to !255139 (closed). The gateway half is gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!7436 (merged) (gitlab-org/modelops/applied-ml/code-suggestions/ai-assist#2766 (closed)).

Screenshots or screen recordings

No UI changes. The one user-visible effect, for get_user called the way GPT calls it:

Before After
Validation error: Provide exactly one of username, id, or me The user is returned

How to set up and validate locally

These steps send GitLab's MCP server the arguments a GPT model sends, so you can see each failure on master and the fix on this branch without an LLM. Run steps 3 to 6 once on master and once on this branch, with gdk restart rails-web after switching.

  1. In a Rails console (gdk rails c), create an MCP token for root. MCP needs an OAuth token with the mcp scope; personal access tokens can't have it.

    user = User.find_by_username('root')
    organization = Organizations::Organization.first
    app = Authn::OauthApplication.create!(name: 'MCP repro', redirect_uri: 'http://localhost', scopes: 'mcp',
      owner: user, organization: organization)
    puts OauthAccessToken.create!(application: app, resource_owner: user, scopes: 'mcp',
      organization: organization, expires_in: 2.hours).plaintext_token
  2. In a shell, store the token and add a helper that prints whether the tool call failed and the start of what it returned:

    export TOKEN=<token from step 1>
    mcp() {
      curl -s http://gdk.test:3000/api/v4/mcp -H "Authorization: Bearer $TOKEN" \
        -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
        -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":$1}" |
        jq -c '.result | {isError, text: .content[0].text[0:120]}'
    }
  3. Look up a user by username, with the id: 0 GPT sends next to it:

    mcp '{"name":"get_user","arguments":{"username":"root","id":0,"me":false}}'
    • master: Validation error: Provide exactly one of username, id, or me
    • This branch: the root user
  4. List tags with first: 0:

    mcp '{"name":"list_tags","arguments":{"project_id":"flightjs/Flight","first":0}}'
    • master: Validation error: first is invalid
    • This branch: the tags
  5. Create a file with the empty old_str and new_str GPT adds to every action:

    mcp '{"name":"add_commit","arguments":{"project_id":"flightjs/Flight","branch":"mcp-zero-fill-repro","start_branch":"master","commit_message":"MCP zero-fill repro","actions":[{"action":"create","file_path":"mcp-zero-fill-repro.txt","content":"hello","old_str":"","new_str":""}]}}'
    • master: Partial edits are only supported for update actions
    • This branch: the new commit, on a new mcp-zero-fill-repro branch. Delete the branch afterwards.
  6. Check that milestone_id: 0 still removes a milestone, which this MR keeps for save_merge_request. Pick a merge request that has a milestone (or give one a milestone first) and use its project and IID:

    mcp '{"name":"save_merge_request","arguments":{"project_id":"toolbox/gitlab-smoke-tests","merge_request_iid":1,"milestone_id":0}}'
    • Both: Merge request updated successfully via save_merge_request., and the merge request no longer has a milestone.
  7. Optional, with a GPT model: if your GDK's AI gateway has an OpenAI key, open Agentic Chat in flightjs/Flight, pick a GPT model, and ask:

    • Use the get_user tool to look up the user root.
    • Use the add_commit tool to create a file called gpt-test.txt with content "hello" on a new branch gpt-test from master, with commit message "GPT test commit".

    On master, Terri's GPT run hit the error from step 5 with the add_commit prompt. On this branch both calls should succeed; log/api_json.log shows the arguments GPT sent. I haven't run this step myself, because my GDK has no OpenAI key.

Automated checks
  • spec/services/mcp, ee/spec/services/mcp, ee/spec/services/ee/mcp, spec/requests/api/mcp, ee/spec/requests/api/mcp, ee/spec/lib/api/helpers/duo_workflow_helpers_spec.rb and spec/lib/feature/definition_spec.rb pass.

To try the gateway flag once ai-assist!7436 is deployed:

Feature.enable(:dap_openai_non_strict_tools, User.find_by_username('your-username'))

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Fred de Gier

Merge request reports

Loading
Loading