Skip to content

validate_ci_lint throws on every invalid config: merged_yaml/includes are null, schema uses .optional() #638

Description

@nkz-soft

Summary

validate_ci_lint throws instead of returning a result whenever the CI config is invalid — that is, in exactly the case the tool is useful for. On a valid config it works fine, so the bug stays invisible until the user actually has a broken .gitlab-ci.yml.

Cause

GitLab returns merged_yaml: null and includes: null when the config is invalid. GitLabCiLintResultSchema declares both as .optional():

https://github.com/zereight/gitlab-mcp/blob/main/schemas.ts#L317-L324

export const GitLabCiLintResultSchema = z.object({
  valid: z.coerce.boolean(),
  errors: z.array(z.string()),
  warnings: z.array(z.string()).optional(),
  merged_yaml: z.string().optional(),
  includes: z.array(z.unknown()).optional(),
  jobs: z.array(z.unknown()).optional(),
});

Zod's .optional() accepts undefined but rejects null, so .parse() throws in validateCiLint/validateProjectCiLint and the tool surfaces a schema error rather than the lint result.

This is the same class of bug as #575 (role-based protected-branch access levels), fixed there by allowing null.

Reproduction

Any invalid config will do. This one uses a job named image, which collides with the default: image: block:

default:
  image: alpine:3.20

stages:
  - build

image:
  stage: build
  script:
    - echo build

Direct APIPOST /projects/:id/ci/lint:

{"valid":false,"errors":["image is defined in top-level and `default:` entry"],"warnings":[],"merged_yaml":null,"includes":null}

Through the MCP toolvalidate_ci_lint with the same content:

{"code":-32603,"message":"Invalid arguments: merged_yaml: Expected string, received null, includes: Expected array, received null"}

The actual explanation — image is defined in top-level and `default:` entry — never reaches the caller.

Expected

The tool returns the lint result with valid: false and the errors array, so the caller can see why the config is rejected.

Impact

A coding agent using this tool to validate CI changes cannot tell "the config is invalid" from "the tool is broken". In our case the agent concluded the tool was broken, silently fell back to prettier --check — which validates YAML formatting, not GitLab CI semantics — and reported success on a config that was invalid. Both error classes it needed to catch (a job named image, and needs pointing at a nonexistent job) are caught by the lint endpoint in under a second and are invisible to a formatter.

Environment

  • @zereight/mcp-gitlab 2.1.42 and 2.1.46 — reproduced on both
  • self-hosted GitLab 19.2.0-ee
  • transport: stdio, PAT auth, GITLAB_PERMISSION_MODE=modify
  • client: Claude Code

Suggested fix

.nullish() on the two fields GitLab actually nulls:

-  merged_yaml: z.string().optional(),
-  includes: z.array(z.unknown()).optional(),
+  merged_yaml: z.string().nullish(),
+  includes: z.array(z.unknown()).nullish(),

Happy to open a PR with a regression test in test/nullable-gitlab-response-fields.test.ts alongside the #575 cases.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions