Skip to content

Tool Reference

The server has two surfaces: HTTP endpoints for session lifecycle and diff upload, and MCP tools for the reviews themselves. Your client calls both for you — this page is the reference for building against the server directly, or for adding a client CloudAEye does not yet ship a guide for.

The base URL and the MCP transport URL are the same string, e.g. https://api.cloudaeye.com/mcp.


HTTP endpoints

Both endpoints require a CloudAEye product API key in an X-Product-API-Key header, and the tenant_key the key belongs to. The key must carry that tenant on its record and grant the Code Review product; a mismatch answers 403.

POST /session

Mint or resume a session. A session is identified by (tenant, repository, developer) — not by branch or head. One developer working on one repository has exactly one session, and it survives branch switches and commits. The client sends no session id and holds no state; the server resolves the same session every time.

Request body (JSON):

Field Required Description
repo yes Repository name
branch yes Current branch. Refreshed on every resume — state, not identity
head yes Current git rev-parse HEAD. Also refreshed on resume; it is what the context refresh fetches base file contents against
tenant_key yes Your CloudAEye tenant. Selects the database holding the repo integration, the code-context graph and the Jira installation
user_name no Scopes the session, so two developers on one repository don't share one
pr_number no Review an open pull request of this repository instead of the working tree. The server fetches the diff itself, so no upload step follows. Part of the session's identity, so a PR session never overwrites your working-tree one
language no Legacy override. Leave it out — the server derives it from the diff.

Response:

{
  "session_id": "…",
  "resumed": false,
  "target_branch": "main",
  "repo_full": "owner/name"
}

target_branch and repo_full come from your tenant's integration record — the bare repo name you sent is resolved into owner/name, and target_branch is the review baseline.

When the repository is not integrated, the response carries target_branch: "" plus target_branch_error and an integration_url naming where to connect it. This is not an error: the session still works, and the client falls back to diffing against local HEAD.

On a resume where a task was previously checked, the response also includes a prior_task block (task_description, task_source) so a client can offer to reuse it instead of re-prompting.

With pr_number set, the response carries a pull_request block — number, title, draft, base, head, head_sha, changed_files — and the diff is already staged, so skip POST /upload. The pull request must be open (drafts count), merge into your integrated branch, not come from a fork, and change at most 50 files; anything else answers status: error with a message naming which rule it broke. Every tool below works on a PR session — inspect_diff, slop_score, control_flow, review_arch, describe_change, ask and check_task — and control_flow and review_arch are the two that can post back to it. change_risk works only on a PR session, and can post back to it too. Fix planning (/cloudaeye:implement) does not: its plans describe edits to a working tree a PR session has none of.

Errors: 400 if the body is not JSON or a required field is missing; 401 for a missing, unknown or expired key; 403 if the key does not belong to the named tenant or lacks the Code Review product; 503 if the server cannot reach its database.

POST /upload/{session_id}

Replace the diff for an existing session. Multipart, two fields: file (the diff) and base_sha (the commit it was taken against — the server stages file contents against it).

curl -s -F "file=@session.diff" -F "base_sha=$BASE" \
  -H "X-Product-API-Key: $CLOUDAEYE_API_KEY" \
  https://api.cloudaeye.com/upload/$SESSION_ID

Response:

{ "session_id": "…", "diff_unchanged": false }
  • diff_unchanged: true when the uploaded bytes are identical to the last upload and the cached context is still on disk — the server skips the cache wipe.
  • Otherwise the cached code-context graph and staged post-edit file tree are invalidated, so the next tool call re-derives them against the new diff.

Errors: 404 if the session is unknown — call POST /session first; 400 if the file field is missing.

The diff is never passed inline to an MCP tool. It lives on the server keyed by session_id. Always derive it from git diff rather than constructing it by hand — see Deriving the diff.


MCP tools

The tools below cover the review flows. All return a JSON string. The session tools take a session_id and reference the uploaded diff by it; they are not key-authenticated, since a session_id is only obtainable by a caller who authenticated to POST /session. Three tools — check_pr, add_docs, add_tests — take no session at all: they name a repository and a pull request, and the hosted service fetches everything itself. They are described at the end.

inspect_diff

One tool covers all three review passes. profile decides which checks run.

inspect_diff(session_id: str, intent: str = "", profile: str = "inspect",
             context: dict | None = None) -> str
Argument Description
session_id Id from POST /session
intent Optional. A one-line note on what the agent just changed — steers what the planner prioritises, and on a recheck says which findings were already addressed
profile inspect (default), security, or review. An unknown value falls back to inspect, never to a wider set
context Optional per-call extras: pr_title, pr_description, review_config, report_types
profile Report types
inspect BUG_REPORT — no security checks
security SECURITY_REPORT, LLM_SECURITY_REPORT, AIAGENT_SECURITY_REPORT, MCP_REPORT, SECRET_REPORT
review all of the above

Returns: verdict, findings[], profile, session_id, and counts when there are findings.

Everything else appears only when something didn't do what you'd assume — degraded, context_refresh, secret_scan, compiler_check, cached. No field means it went fine.

slop_score

Scores the session's diff for signs of low-quality code and returns a rendered card.

slop_score(session_id: str, run_review: bool = True,
           context: dict | None = None) -> str
Argument Description
session_id Id from POST /session — a working-tree session or a pull-request one
run_review Leave true. The score is arithmetic over review findings, so the review has to have run on this diff; false scores only what is already stored, which on a fresh session is every signal unmeasured
context Optional per-call extras: scope_path, the path the diff was narrowed to. It filters nothing server-side — you uploaded an already-narrowed diff — but it is what makes the response say what it covered

Nine signals — duplicate code, hallucinated API, dead code, missing tests, high complexity, generic naming, boilerplate comments, overengineering, style inconsistency — scored out of 100. Whatever inspect_diff already worked out on the same diff is reused rather than recomputed.

Returns:

Field Meaning
card The rendered card. Print it verbatim — the rules about what may be shown are part of the output: an unmeasured signal renders as a dash and a reason rather than a bar at zero, and a partially covered signal says so on its own row
total The score out of 100, as the card prints it. Not calibrated yet, and the card says so
band One of the four review bands
signals Each signal's finding count; null for a signal nothing measured
kinds Findings by category, so a caller can say 2 missing tests · 1 dead import without reading the card
unmeasured Every signal nothing looked at, with the reason. Never trimmed, and never to be reported as clean
headline The size and shape of the change — lines, files, and how many changed functions carry a behavioural edit

band_source, band_reason and arithmetic_band appear only when the band was moved after the evidence was weighed. scope appears only when the diff was narrowed, and degraded only when the review underneath could not read your post-edit source — which makes the whole card unusable, not merely low. When nothing at all could be measured there is no band, and a reason field says why.

There is deliberately no min_severity. A floor drops findings before they are counted, so every signal's numerator would shrink while its denominator stayed put, and the score would fall because less was reported rather than because less was wrong.

How the score is built — each signal's weight and the points it earned — is not part of the result or the card.

control_flow

Draws what the session's diff did to the flow of control, as before/after sequence diagrams, and returns a rendered card.

control_flow(session_id: str, depth: int = 2, label: bool = True,
             cross_repo: bool = True, post: bool = False,
             context: dict | None = None) -> str
Argument Description
session_id Id from POST /session — a working-tree session or a pull-request one
depth Call hops to follow from the flow's root. 2 is the readable default; 3 is usually past what a reader can hold
label Make the one model call — it chooses which flows to draw, names them, and writes the opening sentence. false draws the highest-reaching flow with its raw symbol name and costs nothing
cross_repo Also search the tenant's other connected repositories for callers this change may have broken. One confirm call plus a graph read per sibling. Never silently skipped when true
post Put the card on the pull request as a comment through the CloudAEye App. Writes where other people can see it, so off by default; needs a session opened with pr_number, and a working-tree session is refused rather than ignored
context Optional per-call extras: scope_path, the path the diff was narrowed to. Filters nothing server-side — you uploaded an already-narrowed diff — but is what makes the response say what it covered

The arrows are read off the repository's stored code graph, where every call site carries a line, so they are resolved facts rather than a reconstruction from the patch. Exactly one model call is made, and it never adds, removes or reorders a message: it chooses among finished pictures, names them, and summarises. Its failure leaves the diagram unchanged and the opening line written from the counts.

Returns:

Field Meaning
card The rendered card. Print it verbatim, every mermaid fence included — it is the same block that gets posted on the pull request, and the rules about what it may show are part of the output: the opening line, the facts table, the before/after pair, the marked arrows
changed Counts of added, removed and retargeted messages across both pictures
messages Messages on the after picture
flow The flow drawn — a route label like POST /api/v1/orders when the root is a handler, otherwise the symbol name
diagrams 2 when the card holds a Before and an After. Absent when it holds one
flows / drawn How many independent flows the change has, and how many the card drew. drawn: 0 is a refusal the card explains
unresolved Calls the graph could not follow — drawn as dashed arrows, counted here, never listed
pr_number / pr_url Only on a pull-request session. pr_url is constructed, never read from a payload
posted Only with post: true. status is ok when the card is on the pull request; anything else means it is not, and says why

diff_measured: false rides the result when there was no pre-edit graph to compare against — every marker was suppressed, and that is the opposite claim from "nothing changed". cross_repo appears with affected, repos and searched when callers were found in other repositories, and with unmeasured (and not_indexed) when the search could not run or could not see every sibling — never as a silent absence. scope appears only when the diff was narrowed, and context_refresh only when the graph was not refreshed with this diff.

unavailable with a note is an ordinary answer: no code graph, no changed symbol the graph carries, or a change whose symbols make no resolvable calls. It is not an error to retry.

review_arch

Reviews what the session's diff did to the repository's architecture, and returns a rendered card.

review_arch(session_id: str, post: bool = False,
            context: dict | None = None) -> str
Argument Description
session_id Id from POST /session — a working-tree session or a pull-request one
post Put the card on the pull request as a comment through the CloudAEye App. Writes where other people can see it, so off by default; needs a session opened with pr_number
context Optional per-call extras: pr_title on a working-tree session (a pull-request session carries its own), and scope_path when the diff was narrowed to a path

The layers and components come from the repository's architecture model, and the edges are read out of its code graph before and after the change, so every dependency on the card is a resolved fact. Two model calls are made — one confirms convention candidates, where no verdict drops the candidate; one writes the walk-through and answers the questions, where no answer leaves the measured card with its questions printed as questions — and neither can add a file, a layer or an edge.

Returns:

Field Meaning
card The rendered card. Print it verbatim, every mermaid fence included — the system, the repository and the change pictures, the numbered findings, Across the tenant, the questions, and a footer that counts pre-existing violations and names the model the change was graded against
verdict PASS, FINDINGS or FAIL. FAIL needs a layer the repository declares on both ends of a violation; an inferred layer never fails a change
findings / by_kind How many findings, and how many of each kind
layers_touched / components_touched / deployables_touched What the change reached
pre_existing Violations already in the touched files — counted, never listed. A clean result over a large number is a clean change in a repository already in violation
questions How many questions the card asks
model source (stored or built from this session's graph), labelled (false means components are named by directory), and labelling (started or in_progress while they are being named in the background)
changed_files How many changed files the review read
pr_number / pr_url Only on a pull-request session
posted Only with post: true. status is ok when the card is on the pull request; anything else means it is not, and says why

Each of these appears only when it applies: - diff_measured: false: there was no pre-edit graph, so no edge is marked added and cycles and direction were not compared. That is the opposite claim from "nothing changed". - confirm_failed / explain_failed: the convention candidates were dropped, or the questions went unanswered. Neither is a clean result. - system_map counts the services read from the tenant's deployment files, with measured: false and a reason when the system picture could not be drawn. - service_map counts the connected repositories found calling this one. - cross_repo counts breaks in other repositories.

The last two carry measured: false when they did not run, never a silent absence.

unavailable with a note is an ordinary answer: no code graph, no architecture model that could be loaded or built, or a change with no source code in it. That last one says what the change touched instead (touched: manifests, deployment files, CI, docs), and unread_languages names changed code in a language the review does not read yet — a gap in what was reviewed, not a clean result.

change_risk

Whether a pull request needs a careful human review, why, and who to ask: a risk score from who wrote it, what it touches and what those files and that author have been through over a window of weeks, together with the pull request's own slop score.

change_risk(session_id: str, days: int = 21, post: bool = False,
            context: dict | None = None) -> str
Argument Description
session_id Id from POST /session, opened with pr_number. A working-tree session is refused: the history is the pull request's repository's
days The window, 7 to 90. Every trend is one value a week inside it
post Put the card on the pull request, editing CloudAEye's earlier risk card there rather than adding a second. Writes where other people can see it, so off by default
context Optional per-call extras

It is queued: the call returns {job_id, status: "queued"}, and the result below comes back under result from get_job_result(job_id) — poll that, never call change_risk again to wait, which queues a second run. It reads git and the pull requests on GitHub, the repository's GitHub issues and, when the tenant has the CloudAEye Jira app, the Jira bug tickets in the window. The only model spend is the slop review of this pull request, and only when none exists for its current commit — one already run, or a risk card already on the pull request for that commit, is reused.

Returns:

Field Meaning
card The rendered card. Print it verbatim — it leads with the verdict, then what needs attention, then the evidence week by week, and names what it could not measure
score The risk score out of 100, as the card prints it. Not calibrated yet, and the card says so
band low, medium or high; empty when too little was measured to name one — never read that as low
verdict review (human review required) or approve (no extra review needed)
next_step The one action to suggest, chosen by rule from what decided the verdict
slop This pull request's slop score: score, band, and ran — ran when the review was run for this card, reused when an earlier one at this commit was read. {measured: false, reason} when it could not be scored, which rules out approve
unmeasured Each input that could not be measured, as {what, why}. Left out of the score, never to be reported as clean
reviewers Up to two people to ask, as {name, why}. name is a GitHub login only where the card prints it with @
census What the window held: bugs (bugs, mapped, unmapped — no note yet saying which files the bug was in, so not in the score — and the sources read) and author_prs
pr_number / pr_url The pull request
posted Only with post: true. status is ok when the card is on the pull request; anything else means it is not, and says why

Every call answers in this shape, whether or not a card is already on the pull request. How the score is built — weights, points per row — is not part of the result or the card.

describe_change

A plain-markdown description of the session's diff.

describe_change(session_id: str, intent: str = "", context: dict | None = None) -> str
Argument Description
session_id Id from POST /session
intent Optional. The user's original task request, verbatim
context Optional extras (pr_title, pr_description)

Returns: a description field — a # Change Description paragraph plus an ## Important Changes bullet list. Trivial diffs short-circuit to a one-line "Trivial change —" summary. Also returns files_visited, an eval_summary, and a context_refresh summary when the graph refresh was skipped or failed.

ask

Answers a free-form question about the session's diff, against the refreshed code-context graph.

ask(session_id: str, question: str, intent: str = "",
    context: dict | None = None) -> str
Argument Description
session_id Id from POST /session
question The user's question, passed verbatim. Do not summarise, expand or rewrite it — the phrasing carries what they want to know, and a rewritten question gets a different answer
intent Optional summary of what the agent just changed. Context for the question, not its subject
context Optional extras (pr_title, pr_description)

Returns: a plain-markdown answer plus files_visited. This is not a review — no verdict, no findings. For findings use inspect_diff.

check_task

Verifies whether the session's diff fulfils a stated task.

check_task(session_id: str, task_description: str,
           task_source: str = "user-supplied",
           intent: str = "", context: dict | None = None) -> str
Argument Description
session_id Id from POST /session
task_description The task to verify against — a ticket reference list ([BETA-5225, #42], a bare Jira key, owner/repo#42, or a GitHub issue URL), or freeform text passed verbatim
task_source Provenance label: github-issue, jira, spec, or user-text
intent Optional. A one-line note on this round of edits; on a recheck, which prior gaps were addressed
context Optional extras (pr_title, pr_description)

Returns: a report field — a # Task Completion Check heading, an overall verdict (DONE / PARTIAL / NOT DONE), a per-requirement checklist, and any gaps. Tickets are re-fetched on every call.

Errors: returns a structured { "error": … } rather than a verdict when task_source=github-issue but the URL is unrecognised, when a ticket cannot be resolved, or when a task made only of unresolvable ticket references has nothing to judge. See Custom Context.

create_issue

Files findings from the session's last review as tracking issues.

create_issue(session_id: str, request: str, provider: str = "jira",
             assignee: str = "", note: str = "") -> str
Argument Description
session_id Id from POST /session. A review must already have run on it; the findings come off its stored report, by the numbers that report printed
request Which findings — "[2]", "[1,3]", "[2-4]" — in implement's grammar, resolved in Python against the stored report. Required, and free text is refused: a wrong number here puts somebody's name on a ticket about the wrong defect. Unlike implement, an empty request is refused rather than read as "all of them"
provider jira (default) or github. Jira goes through the CloudAEye external service and works on a working-tree session as well as a pull-request one; GitHub is the one direct write this server makes to a repository, and files against the session's repository
assignee Optional. A Jira display name or a GitHub username
note What the user wants said — goes above the finding in the description, as the instruction the finding is evidence for

Returns: three lists, each entry carrying the finding's number and title:

Field Meaning
filed Ticket key and link for each finding filed this call. A GitHub entry may carry assignee_dropped — the issue exists, unassigned, because that user cannot be assigned on that repository (HTTP 422; nothing else is retried as an assignee problem)
already_filed Findings that already have a ticket from this session, with its key. Matched by content — path and title — not by number, so a later review renumbering a finding does not file it twice
failed The error as written. no Jira integration is configuration; issues are disabled on this repository is a GitHub setting

The record lives in the session (filed_issues.json), not in the review report, so it survives a re-review. Filing never closes a finding.


Session-free tools

Three tools take no session_id. They name a repository and a pull request, and the hosted CloudAEye service does the rest — fetches the pull request, runs the job, and posts the result on the pull request through the CloudAEye GitHub App. This server holds no repository credential of its own; the App does, and the only thing this server contributes is what to post. The tenant comes from your OAuth token, never from an argument.

All three refuse the same pull requests the review tools refuse: not open, not merging into the integrated branch, from a fork, or over 50 files. Each answers status: "error" with pr_ineligible and a message naming the rule.

All three can outlive one call on a large pull request. They answer status: "pending" with an id — review_id for check_pr, job_id for the generators — and the caller resumes with that id rather than calling again without it: a second review is billed again and posts its own comments; a second generation run posts a second set of suggestions.

check_pr

Runs the repository's configured hygiene checklist over an open pull request.

check_pr(repo: str, pr_number: int, review_id: str = "", wait_seconds: int = 0) -> str
Argument Description
repo Repository name, as passed to POST /session
pr_number The pull request to check
review_id Resume a review this tool already started — the value from a pending result
wait_seconds How long to wait before returning pending. Defaults to the server's budget

Which checks run is the repository's configuration in the CloudAEye console. There is no argument to enable one, and none is reachable through the API this calls. With nothing enabled the answer is status: "not_configured", with available_checks, before anything is spent — not an empty checklist that renders like a clean one.

Returns:

Field Meaning
verdict approve or request_changes, synthesised here from the check statuses — the service suppresses its own verdict in checklist-only mode. error when the run could not read the pull request's source: report a failed run, print nothing else
summary One line, with the counts and the percentage of any failing coverage check. Never reads clean when degraded is non-empty
checks One row per check: label, status, metric. passed ran and is satisfied; failed ran and found something; skipped is not enabled; error could not run — a coverage gap, not a pass. metric is the check's own number (57.1% (4/7 documented)) or ""; never a figure computed client-side
counts.findings / stages Counts only. The findings' bodies and each check's report are not returned — they are posted on the pull request
posted Whether that posting worked. ok: the detail is on the pull request. partial: some of it. failed: none of it — the table is the whole result. unavailable: the App is not installed on this repository
pr_url Constructed from the repository and number, never read from a payload, so it is present on a resume too
degraded Always carried when non-empty. Checks that could not run

What crosses the wire is the headline, the table and the link. The full result ran ~4,900 tokens, 77% of it per-check markdown the caller is told never to print; trimmed, ~335. That markdown is what gets assembled into the pull-request comment instead.

add_docs

Proposes docstrings for the undocumented code a pull request touches, on the pull request.

add_docs(repo: str, pr_number: int, job_id: str = "", wait_seconds: int = 0) -> str
Argument Description
repo / pr_number As for check_pr
job_id Resume a run this tool already started — the value from a pending result
wait_seconds As for check_pr

Returns:

Field Meaning
summary One line with the counts, as the service wrote it
counts.suggestions What the service generated
files Per-file rows: path, suggestions, and new_file for a whole new file
posted What reached the pull request — a different number, and the only one that sends anyone to look. comments is how many landed; fallbacks, within it, how many went up as plain comments because their line is outside the diff (GitHub refuses a suggestion there), so the text is present and the apply button is not. status as for check_pr
unattached Suggestions that came back with no file, when any

The generated text is never returned. An earlier version of this tool returned it to the coding agent to apply — a docstring written by a model on the server, read by a model on the client, and retyped into an edit, with an unbounded payload. The shape that survives that objection is the service's own: the suggestion is posted where a click applies it, and no diff passes through a context window. A change that starts returning suggestion bodies re-creates the deleted tool.

Zero suggestions is an ordinary answer on a well-documented change, reported as "nothing was added" and never as a pass. empty_result_message is always sent, so a run that generates nothing still leaves a trace on the pull request.

add_tests

Proposes unit tests for the changed code nothing covers, on the pull request.

add_tests(repo: str, pr_number: int, job_id: str = "", wait_seconds: int = 0) -> str

Same arguments, same return shape and same rules as add_docs, with two differences:

  • files rows marked new_file are whole new test files. GitHub takes no inline suggestion on a path outside the diff, so those go up as plain comments — the same fallback out-of-diff lines take.
  • Zero suggestions means nothing in the change was found that a new test would cover. That is not "the code is tested", and the docstring says so in as many words.

The stored result is keyed by (tenant, repository, pull request), not by job — one document, shared by every run of either generator and by @cloudaeye /test on the pull-request timeline. A finished document is accepted only when its job_id matches the one this call's request returned, so a concurrent run finishing while this one polls cannot hand back its counts.


Deriving the diff

Always derive the diff from git diff rather than constructing it manually, so the server sees exactly what git sees.

git add --intent-to-add . && git diff > session.diff

git add --intent-to-add is non-destructive — it registers new files in the index without staging their content, so git diff emits them as new file hunks. Without it, completely untracked files are invisible to the diff and to the review. Undo with git reset HEAD <file> if needed.


End-to-end example

CE=https://api.cloudaeye.com/mcp
REPO=$(basename -s .git "$(git config --get remote.origin.url)")
BRANCH=$(git rev-parse --abbrev-ref HEAD)
HEAD_SHA=$(git rev-parse HEAD)

# 1. Create or resume the session. The response names the baseline branch.
curl -s -X POST "$CE/session" \
  -H "Content-Type: application/json" \
  -H "X-Product-API-Key: $CLOUDAEYE_API_KEY" \
  -d "{\"repo\":\"$REPO\",\"branch\":\"$BRANCH\",\"head\":\"$HEAD_SHA\",\"tenant_key\":\"$CLOUDAEYE_TENANT_KEY\"}" \
  > session.json
SESSION_ID=$(python -c "import json;print(json.load(open('session.json'))['session_id'])")
TARGET=$(python -c "import json;print(json.load(open('session.json')).get('target_branch') or '')")

# 2. Resolve the baseline: the fork point off the integrated branch, not its tip.
#    Validate $TARGET first — it comes off the wire, and a leading dash reaching
#    git fetch is command execution. No integration, no branch: fall back to HEAD.
BASE=""
case "$TARGET" in -*|*[!A-Za-z0-9._/-]*) TARGET="";; esac
if [ -n "$TARGET" ]; then
  git rev-parse --verify -q "origin/$TARGET" >/dev/null || git fetch -q origin "$TARGET"
  BASE=$(git merge-base "origin/$TARGET" HEAD)
fi
[ -n "$BASE" ] || BASE=$HEAD_SHA

# 3. Capture and upload the diff, with the commit it was taken against.
git add --intent-to-add . && git diff "$BASE" > session.diff
curl -s -F "file=@session.diff" -F "base_sha=$BASE" \
  -H "X-Product-API-Key: $CLOUDAEYE_API_KEY" \
  "$CE/upload/$SESSION_ID"

# 4. Call inspect_diff via your MCP client, passing session_id + intent + profile

Steps 1–3 are plain HTTP; step 4 is an MCP tool call your client makes. The Claude Code plugin bundles all four into one slash command; a client without that mechanism runs the first three itself.