Part 7: Self-Correcting MCP Errors, Required-Param Prompts & Strict JSON Schema Fixes in GitLab
Series Context: Part 7 of 10 in GitLab Core Engineering: Architecture, MCP & CI/CD Deep Dives. Featured MRs: !264852, !264853, !264864 (All Merged into master)
Designing Tools for LLM Clients Is Different From Writing REST Docs
When a human developer hits 400 Bad Request on a REST API, they open browser docs or curl logs. When an AI agent calls an MCP tool and gets "Validation error: unexpected_argument is invalid", the model only sees that single string in its next reasoning step.
If an MCP error omits which parameter names are valid - or emits non-standard JSON Schema types - clients fail silently or burn 3-5 round trips guessing. Three recent merged MRs fixed high-leverage friction points across GitLab's MCP stack.
1. Self-Correcting JSONSchemer Validation Errors (!264853, Merged)
Look at how Mcp:🛠️:Base::BaseService#validate_arguments! previously formatted JSONSchemer errors when additionalProperties: false rejected a misspelled parameter:
# Before (app/services/mcp/tools/base/base_service.rb)
validations = errors
.map do |error|
case error['type']
when 'required'
"#{error['details']['missing_keys'].first} is missing"
when 'maxItems' ...
when 'enum' ...
else
field = error['data_pointer'].sub(%r{^/}, '')
"#{field} is invalid"
end
end.uniq
raise ArgumentError, validations.join(', ')
Spot two problems hiding right there:
- When an LLM passed
{ unexpected_argument: "foo" }, JSONSchemer emitted an error withtype == "schema"andschema_pointer == "/additionalProperties". That fell into theelsebranch and told the client only:Validation error: unexpected_argument is invalid- without saying it was an unknown parameter and without telling the agent what parameters are accepted! - On 'required' errors,
error['details']['missing_keys'].firstsilently dropped the 2nd and 3rd missing required keys if multiple fields were omitted.
Here is the refactored implementation shipped in !264853:
def validate_arguments!(arguments)
schemer = JSONSchemer.schema(input_schema)
json_arguments = arguments ? arguments.deep_stringify_keys : {}
errors = schemer.validate(json_arguments).to_a
return if errors.empty?
validations = errors.flat_map { |error| format_validation_error(error) }.uniq
validations << accepted_parameter_hint if errors.any? { |error| unknown_parameter_error?(error) }
raise ArgumentError, validations.join('; ')
end
def format_validation_error(error)
field = error['data_pointer'].sub(%r{^/}, '')
return "#{field} is invalid: unknown parameter" if unknown_parameter_error?(error)
case error['type']
when 'required'
error['details']['missing_keys'].map { |key| "#{key} is missing" }
when 'maxItems'
"#{field} cannot contain more than #{error['schema']['maxItems']} items"
when 'enum'
"Invalid #{field}: '#{error['data']}'. Must be one of: #{error['schema']['enum'].join(', ')}"
else
"#{field} is invalid"
end
end
def unknown_parameter_error?(error)
error['schema_pointer'] == '/additionalProperties' && error['type'] == 'schema'
end
def accepted_parameter_hint
accepted = accepted_parameter_names
return 'This tool takes no parameters' if accepted.empty?
"Accepted parameters: #{accepted.join(', ')}"
end
def accepted_parameter_names
properties = input_schema[:properties] || input_schema['properties'] || {}
properties.keys.map(&:to_s)
end
Now look at what an AI client receives in a single failed turn:
Validation error: unknown_param is invalid: unknown parameter; required_field is missing; Accepted parameters: required_field, optional_field, items_field, enum_field
Notice every edge case handled cleanly:
- Multiple unknown parameters (
unknown_one,unknown_two): Each unknown key is named individually, while "Accepted parameters: ..." appears only once at the end. - Zero-argument tools (
accepted.empty?): Emits 'This tool takes no parameters' instead of a dangling empty list. flat_maponmissing_keys: Reports every missing required parameter in one pass.
2. Leading Tool Descriptions With Required Parameters (!264852, Merged)
Even with strict schemas, weaker or fast tool-calling models pay disproportionately higher attention to the opening two sentences of an MCP tool's description. In app/services/mcp/tools/search/search_service.rb, the description_parts array led with "Search across GitLab with automatic selection of the best available search method." followed immediately by long capability strings and syntax guides (filename:*.rb, extension:js).
Agents frequently invoked search with guessed parameter names like query or omitted scope. !264852 added one single, high-signal second line upfront:
def description_parts
[
'Search across GitLab with automatic selection of the best available search method.',
'Requires `scope` and `search` (for example, `{"scope": "work_items", "search": "rate limit"}`).',
'',
"**Capabilities:** #{search_capabilities.join(', ')}",
...
Small diff - massive reduction in first-turn search schema mistakes!
3. Fixing Grape Date Schema Type & Adding ungovernable: true for User-Scoped API Tools (!264864, Merged)
When exposing GET /users/:id/events (lib/api/events.rb) as the list_user_events MCP tool in !264864, generating the tool's inputSchema hit two framework bugs in Mcp:🛠️:Base::ApiTool (app/services/mcp/tools/base/api_tool.rb):
Bug A: Grape Date Emitted Invalid "type": "date" in JSON Schema. The events endpoint declares query parameters before and after with Grape type Date. ApiTool::TYPE_CONVERSIONS mapped 'DateTime' => 'string', but did not include 'Date', so parse_type fell through to type.downcase and emitted:
{ "before": { "type": "date" } }
"date" is not a valid JSON Schema primitive type (JSON Schema defines string with optional format, never "date"). Strict MCP hosts (IDE clients, SDK validators) reject the entire tools/list response when any tool schema fails JSON Schema validation!
Adding 'Date' => 'string' fixed schema conformance globally for every future Grape endpoint with Date params:
TYPE_CONVERSIONS = {
'[String, Integer]' => 'string',
'Grape::API::Boolean' => 'boolean',
'[Integer]' => { type: 'array', items: { type: 'integer' } },
'[String]' => 'string',
'Date' => 'string',
'DateTime' => 'string'
}.freeze
Bug B: User-Scoped Endpoints vs Project/Group Governance (ungovernable: true). ApiTool expects container-governed tools to resolve either a :project or :group boundary via GovernanceContainerResolver. However, /users/:id/events has boundary_type: :user - it operates across user activity, not a single project or group!
Adding declarative ungovernable: true support on route_setting :mcp cleanly bypasses container checks for user-scoped tools without duplicating governance overrides:
# app/services/mcp/tools/base/api_tool.rb
override :ungovernable?
def ungovernable?
return true if settings[:ungovernable]
super
end
override :container_arguments
def container_arguments
return {} if ungovernable?
...
end
And documented right alongside service-based tools in doc/development/duo_agent_platform/mcp/_index.md!
Takeaways for GitLab Engineers
- Make validation errors teachable: Always tell the MCP caller what key was unexpected and what keys are valid in the same error string.
- Put minimal valid examples (
{"scope": "...", "search": "..."}) in line 2 of tool descriptions. - Never emit Grape Ruby class names verbatim into JSON Schema: Always audit generated inputSchema output for non-standard primitive types like "date".
Series Navigation
- Part 1: Taming EE::Project & Extracting Ai::Catalog (!269060)
- Part 2: Decoupling EE::Group Routing via Ai::DuoSettings::Paths (!269059)
- Part 3: Escaping Non-ASCII Git Refs in Workspaces - AppSec CWE-78 & CWE-116 (!262103)
- Part 4: Exposing Grape Routes to MCP - list_pipeline_schedules & Auth Traps (!264856)
- Part 5: Vacuous RSpec Mocks & Live Upstream Flag Rebases (!269061)
- Part 6: Sequential description_edits & Quick-Action Defense in save_work_item (!264862)
- Part 7 (This Article): Self-Correcting MCP Errors & JSON Schema Fixes (!264852, !264853, !264864)
- Part 8: Consolidating SavePipelineService & Active Polling Hints (!264858, !264863)
- Part 9: Surviving Dangerfile, Commit Trailers & Predictive CI Pipelines
- Part 10: Security Cleanups, Organization empty? & Review Etiquette (!261178, !261030, !264859)
Comments
No comments yet. Start the discussion.