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 checksread_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_lognow 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
How to test locally (GDK)
-
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) -
Make an OAuth token with the
mcpscope: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 -
Check that
get_jobis in the tool list andget_job_logis 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"] -
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, andweb_url, with nologkey. -
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:
returnedis 0 to 50,truncatedis true, andsystem_instructionsays to call again withbyte_offset50. The next call with that offset returns bytes 50 to 100. -
Check that the old name still returns the log with no
includevalue: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
returned0 to 1164 andtruncatedfalse.
I also ran these checks in GDK against a real job:
- A 600000 byte trace with no
byte_limitreturns 512000 bytes andtruncatedis true, under both theget_jobname and theget_job_logalias. byte_limitof 512001 givesValidation error: byte_limit is invalid.- An
includevalue with two items givesValidation 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_limitof 2, returns the replacement character and no error. The whole window returns the check mark unchanged.