The API¶
MCP is the primary surface; REST and CLI are the same flows in other shapes.
Starting the MCP service¶
docker compose --env-file docker/stack.env up -d mcp
# listens on 0.0.0.0:8001; GHAT_MCP_PORT moves it; bind to loopback when local
The tools¶
ci_status what this service can do, and its health
find_runs(repository, ...) runs by commit / PR / branch / workflow / status
get_run(run_id) the run and its job roster
get_job(job_id, refresh_logs=false) one job, every step, and each has_log
get_job_log(job_id, step=N, ...) one step, the failed steps, or the merged document
(a failed job defaults to its failed steps;
whole_job=true asks for the merged document)
get_run_failed_logs(run_id) a run's failed steps, grouped by identical text
search_job_log(job_id, query, step=N) search with line numbers
wait_for_run(run_id, timeout) bounded long poll
trigger_workflow(..., if_absent=false) dispatch
recent_runs(repositories, ...) several repositories in one call
rerun_run(repo, run_id, failed_only) re-run the whole run, or only its failed jobs
cancel_run(repo, run_id) cancel
workflow= takes a path, a file name or the numeric id — never a display
name, because the name differs per branch and the stored one follows the last run
written.
Connecting the agents¶
Claude Code, Codex, OpenCode and Command Code each manage MCP differently —
config file, scope model, approval gates. The exact snippets that connect them
to this service are in Coding agents over MCP; the endpoint is
http://127.0.0.1:8001/mcp (Streamable HTTP) and the authentication is a static
Authorization: Bearer <GHAT_API_KEY> header whenever that variable is set.
Where a run's seconds went¶
duration_seconds is the attempt's wall clock — the window GitHub reports, which
includes whatever the run spent waiting for a runner. Every run read
(get_run / find_runs / recent_runs) also carries timings:
| Field | Answers |
|---|---|
job_span_seconds |
from the first job that started to the last that ended. The duration to budget a wait from; a job that died after twenty seconds ends it for real |
queue_seconds |
how long this attempt waited before it started. "This workflow is slow" and "the runners were busy" call for different responses |
succeeded_job_span_seconds |
the same span over the jobs that passed, for budgeting a wait against a green workflow |
null means the service has not synced the jobs that would answer, never zero.
trigger_workflow and a timed-out wait_for_run publish the same figures as
typical_duration_seconds / observed (p50 / p90), so the number a caller waits
from and the number a run reports are the same measurement.
Authentication¶
Bearer GHAT_API_KEY. An empty key means no auth and warns at startup; on a
public endpoint it must be set.
Prompts that work¶
Show the model the tool, not the shell. Two examples:
Is rhosocial/python-activerecord green on main? Use recent_runs.
Did ce9fac5 pass CI? Use find_runs with a commit_sha.
Keys and pagination¶
limit (max 50) and offset; every list reports truncated and next_offset.
Time¶
Every timestamp UTC with an explicit offset. created_at is GitHub's creation
time, stored_at is when the row was written locally — they answer different
questions.
One attempt¶
Re-runs keep the run id and increment the attempt number; pass attempt=N to
get_run for a past execution. Each attempt's jobs are the ones stored when it
ran.
Common questions¶
- A run that this service never heard about:
find_runsreads GitHub and adopts it, so no run is invisible. - A step with no log: GitHub serves the split for about an hour; after that
the answer is
capture_closedand the reader falls back to the job's merged document. - A very long job: the default window is the last 200 lines and says how much remains.
When a trigger is allowed¶
trigger_workflow requires Actions write access on the bound credential and a
workflow that declares workflow_dispatch. if_absent=true answers "this commit
already ran" without spending minutes.