Skip to content

Agent diagnosis

Start a failing run through the MCP endpoint and ask the server to explain it, the way an agent would.

Source: examples/agent_diagnosis.py. Run it with python examples/agent_diagnosis.py while cereyan serve examples/ is running.

The server's MCP endpoint gives an agent a curated set of tools. This script does what an agent host does over Streamable HTTP: initialise a session, start a run with run_flow, wait for it, and call explain_failure to get the run, its failed task runs, the last error logs, and its events in one answer. Run it against cereyan serve examples/.

import json
import time
import urllib.request

from cereyan import client, flow, get_run_logger, task


@task
def load(rows: int) -> int:
    if rows > 3:
        raise ValueError(f"too many rows: {rows} (limit 3)")
    return rows

A flow that fails for some inputs

@flow
def flaky_load(rows: int = 1) -> int:
    get_run_logger().info("loading %d rows", rows)
    return load(rows)

A minimal MCP client

One JSON-RPC message per POST. The initialize reply carries an Mcp-Session-Id header that later requests send back; the client name from the handshake is what runs record as created_by.

class Mcp:
    def __init__(self, url: str) -> None:
        self.url = url + "/mcp"
        self.session = None
        self.counter = 0

    def rpc(self, method: str, params: dict | None = None, *, notification: bool = False):
        self.counter += 1
        msg = {"jsonrpc": "2.0", "method": method, "params": params or {}}
        if not notification:
            msg["id"] = self.counter
        headers = {"content-type": "application/json"}
        if self.session:
            headers["Mcp-Session-Id"] = self.session
        req = urllib.request.Request(self.url, data=json.dumps(msg).encode(), headers=headers, method="POST")
        with urllib.request.urlopen(req, timeout=10) as resp:
            self.session = resp.headers.get("Mcp-Session-Id") or self.session
            raw = resp.read()
        return json.loads(raw) if raw else None

    def call(self, tool: str, **arguments):
        result = self.rpc("tools/call", {"name": tool, "arguments": arguments})["result"]
        text = result["content"][0]["text"]
        return json.loads(text) if not result.get("isError") else {"error": text}

Diagnose a failure

if __name__ == "__main__":
    mcp = Mcp(client.read_discovery()["url"])
    mcp.rpc("initialize", {"protocolVersion": "2025-06-18", "clientInfo": {"name": "example-agent", "version": "1"}, "capabilities": {}})
    mcp.rpc("notifications/initialized", notification=True)

    run = mcp.call("run_flow", flow="flaky_load", parameters={"rows": 5})["run"]
    assert run["created_by"] == "mcp:example-agent"
    deadline = time.time() + 30
    while time.time() < deadline and client.get_run(run["id"])["state"]["type"] not in ("Completed", "Failed"):
        time.sleep(0.1)

    explained = mcp.call("explain_failure", run_id=run["id"])
    print("verdict:", explained["verdict"])
    assert "too many rows" in explained["verdict"]
    assert any(e["name"] == "run.failed" for e in explained["events"])