Skip to content

MCP/CLI repo read tools serialize the node's error body as a fabricated result (no HTTP status check) #123

Description

@beardthelion

Summary

The MCP repo read tools parse the node response with client.get(...).await?.json().await? and never inspect the HTTP status. When a visibility-gated endpoint returns a non-2xx JSON body (the opaque 404 for a private repo the caller cannot read, or a 5xx), that error body is serialized straight back to the agent as if it were the requested resource. The tool returns Ok, so the agent has no signal that the read was denied or failed and proceeds on a fabricated object.

This is the MCP twin of the gl repo info bug fixed in PR #113 (cmd_info), and it is distinct from #115. #115 is about the clients sending unsigned requests (owner can't authenticate); this is about not checking the status before parsing. They are orthogonal: #115's proposed .get( -> .get_maybe_signed( switch lets the owner reach a 200, but it does not stop a genuine non-2xx (non-owner, deleted repo, node error) from being parsed as a result. cmd_info already used get_maybe_signed and still needed the separate status-check fix in #113.

Reproduced by execution

A throwaway test driving call_tool("repo_get", {owner, name}, node, None) against a mock node returning a JSON 404:

repo_get returned Ok on a 404 — fabricated repo handed to agent:
  Some("{\n  \"message\": \"repository 'owner/myrepo' not found\"\n}")

The dispatch returned Ok with the error body serialized as the repo. The node is correct (get_repo calls authorize_repo_read, repos.rs:269); the client is wrong.

Affected call sites

Confirmed by reading the code (same get(...).await?.json().await? shape, no status check):

  • crates/gl/src/mcp.rs — repo_get (:690), repo_commits (:701), repo_tree (~:713)

The same shape appears on the CLI side (crates/gl/src/repo.rs cmd_commits, the gl pr read subcommands) and other MCP read arms; those were not individually reproduced here. Worth a sweep when fixing.

Fix direction

Mirror the #113 cmd_info fix: keep the Response, capture status, parse with .json().await.unwrap_or_default(), and return an error on non-2xx (surface the node's message) before serializing. Pairs naturally with #115's signing switch — apply both on the same pass so a private repo's owner gets a real 200 and everyone else gets a surfaced error instead of a fabricated object.

Related

Activity

  1. added
    sev:mediumDegraded but workaround exists
    crate:glgl — the contributor CLI
    kind:bugDefect fix — wrong or unsafe behavior
    subsystem:apiNode REST API request/response surface
    on Jun 30, 2026
  2. beardthelion commented on Aug 10, 2026

    @beardthelion
    CollaboratorAuthor

    Scope note, because the sweep this issue defers turns out to reach further than reads.

    All six gl task commands parse before checking status, mutations included: cmd_create (crates/gl/src/task.rs:143), cmd_list (:180), cmd_view (:205), cmd_claim (:218), cmd_complete (:235), cmd_fail (:257). Each is the same client.post(...).await?.json().await? shape with no status() anywhere. send_signed returns Ok for a non-iCaptcha 403 (crates/gl/src/http.rs, pinned by send_signed_returns_non_icaptcha_403_without_retry), so a denial pretty-prints the node's error JSON and the process exits 0. A script keying on the exit code reads a refused claim as a successful one.

    That matters more now: #275 adds a 403 for claiming a task reserved for another agent, so the claim route gains a denial class this client renders as success.

    The affected-sites list here is MCP reads plus a deferred "worth a sweep when fixing", and #187 is the sanitization axis rather than this one (those six commands have no bail! at all, so they are not among its sites). So the write side currently has no owner, while PR #186 already implements it: its read_json converts all six task commands and returns an error on non-2xx.

    Worth knowing if you are picking this up: #261 also rewrites task.rs through a different helper, json_or_denial, and covers four of the six, leaving cmd_list and cmd_view. Both currently report as conflicting with main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:glgl — the contributor CLIkind:bugDefect fix — wrong or unsafe behaviorsev:mediumDegraded but workaround existssubsystem:apiNode REST API request/response surface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions