605856 - get_job MCP Tool

What does this MR do?

Adds a get_job MCP server tool. By default it returns a job's metadat. The trace is an opt-in facet, requested with include: ["log"].

Other points:

  • A metadata request checks read_build, and a log request checks read_build_trace. A trace from a debug-mode job stays restricted to users with write access.
  • A job that does not exist and a job you cannot read give the same message, so you cannot tell one case from the other.
  • A byte window can split a character into two parts. The tool replaces the incomplete part, so the response is always valid JSON.
  • Behaviour change: get_job_log now returns at most 500 KB of the log per call. Before, it returned the whole log. The response tells the caller how to get the next part. This keeps very large traces out of the server and the model context.

References

#605856 (closed)

How to test locally (GDK)

  1. Turn on the MCP server and the AI beta features in rails console:

    ApplicationSetting.current.update!(mcp_server_enabled: true, instance_level_ai_beta_features_enabled: true)
  2. Make an OAuth token with the mcp scope:

    token = Doorkeeper::AccessToken.create!(
      resource_owner_id: User.find_by(username: 'root').id,
      scopes: 'mcp',
      expires_in: 2.hours,
      organization_id: Organizations::Organization.first.id
    )
    puts token.plaintext_token
  3. Check that get_job is in the tool list and get_job_log is not:

    curl -s -X POST "http://gdk.test:3000/api/v4/mcp" \
      -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","method":"tools/list","id":"1"}' | jq '[.result.tools[] | select(.name | test("job")) | .name]'

    Result: ["get_job","get_pipeline_jobs"]

  4. Get a job's metadata. Replace <namespace/project> and <job_id>:

    curl -s -X POST "http://gdk.test:3000/api/v4/mcp" \
      -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","method":"tools/call","id":"1","params":{"name":"get_job","arguments":{"id":"<namespace/project>","job_id":<job_id>}}}' | jq '.result.structuredContent'

    Result: id, name, status, stage, allow_failure, and web_url, with no log key.

  5. Get the first 50 bytes of its log:

    curl -s -X POST "http://gdk.test:3000/api/v4/mcp" \
      -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","method":"tools/call","id":"2","params":{"name":"get_job","arguments":{"id":"<namespace/project>","job_id":<job_id>,"include":["log"],"byte_offset":0,"byte_limit":50}}}' | jq '.result.structuredContent.log'

    Result for a 1164 byte trace: returned is 0 to 50, truncated is true, and system_instruction says to call again with byte_offset 50. The next call with that offset returns bytes 50 to 100.

  6. Check that the old name still returns the log with no include value:

    curl -s -X POST "http://gdk.test:3000/api/v4/mcp" \
      -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","method":"tools/call","id":"3","params":{"name":"get_job_log","arguments":{"id":"<namespace/project>","job_id":<job_id>}}}' | jq '.result.structuredContent.log'

    Result: the log, with returned 0 to 1164 and truncated false.

I also ran these checks in GDK against a real job:

  • A 600000 byte trace with no byte_limit returns 512000 bytes and truncated is true, under both the get_job name and the get_job_log alias.
  • byte_limit of 512001 gives Validation error: byte_limit is invalid.
  • An include value with two items gives Validation error: include cannot contain more than 1 items.
  • A job ID that does not exist gives Job not found: it does not exist or you do not have access to it.
  • A trace that starts with a check mark, read with byte_limit of 2, returns the replacement character and no error. The whole window returns the check mark unchanged.
Edited by Amr Taha

Merge request reports

Loading
Loading