call_forgejo_api double-encodes a string body, breaking every POST/PATCH #2

Closed
opened 2026-07-29 05:57:47 +00:00 by trtmn · 0 comments
Owner

Problem

call_forgejo_api declares body: z.any() and passes the value straight to forgejoRequest, which does JSON.stringify(body). When an MCP client sends body as a JSON string (which happens in practice), the stringify wraps it a second time and Forgejo receives a JSON string where it expects an object.

Reproduced live twice while creating this very repo:

POST /api/v1/user/repos
body: {"name":"forgejo-mcp","private":false,...}
=> HTTP 422 {"message":"[]: json: cannot unmarshal string into Go value of type structs.CreateRepoOption"}

Confirmed mechanism:

JSON.stringify('{"name":"x"}')  =>  "\"{\\\"name\\\":\\\"x\\\"}\""

The repo had to be created with curl instead. This makes the generic escape hatch unusable for every operation that takes a request body whenever the client serializes it as a string - which is the majority of POST/PUT/PATCH operations in the API.

Suggested fix

Coerce string bodies in call_forgejo_api (or in forgejoRequest) before dispatch:

let parsedBody = body;
if (typeof body === "string" && body.trim() !== "") {
  try {
    parsedBody = JSON.parse(body);
  } catch {
    // leave as-is: some endpoints legitimately take a raw string body
  }
}

Prefer doing this in call_forgejo_api so the low-level client keeps pass-through semantics, and note in the tool description that a JSON string is accepted.

Tests

Add a unit test asserting that a string body and the equivalent object body produce identical request payloads, plus an integration test doing a real POST with a body through call_forgejo_api.

Severity

High - breaks the primary advertised capability (100 percent API coverage) for any write operation.

## Problem `call_forgejo_api` declares `body: z.any()` and passes the value straight to `forgejoRequest`, which does `JSON.stringify(body)`. When an MCP client sends `body` as a JSON *string* (which happens in practice), the stringify wraps it a second time and Forgejo receives a JSON string where it expects an object. Reproduced live twice while creating this very repo: ``` POST /api/v1/user/repos body: {"name":"forgejo-mcp","private":false,...} => HTTP 422 {"message":"[]: json: cannot unmarshal string into Go value of type structs.CreateRepoOption"} ``` Confirmed mechanism: ``` JSON.stringify('{"name":"x"}') => "\"{\\\"name\\\":\\\"x\\\"}\"" ``` The repo had to be created with curl instead. This makes the generic escape hatch unusable for every operation that takes a request body whenever the client serializes it as a string - which is the majority of POST/PUT/PATCH operations in the API. ## Suggested fix Coerce string bodies in `call_forgejo_api` (or in `forgejoRequest`) before dispatch: ```js let parsedBody = body; if (typeof body === "string" && body.trim() !== "") { try { parsedBody = JSON.parse(body); } catch { // leave as-is: some endpoints legitimately take a raw string body } } ``` Prefer doing this in `call_forgejo_api` so the low-level client keeps pass-through semantics, and note in the tool description that a JSON string is accepted. ## Tests Add a unit test asserting that a string body and the equivalent object body produce identical request payloads, plus an integration test doing a real POST with a body through `call_forgejo_api`. ## Severity High - breaks the primary advertised capability (100 percent API coverage) for any write operation.
trtmn 2026-07-29 14:06:51 +00:00
  • closed this issue
  • added the
    CR2
    P1
    labels
Sign in to join this conversation.
No description provided.