Skip to content

Adding an Agent

Every agent follows the same pattern. Here's the complete recipe.


Step 1: Write the Agent

Create morning_agents/agents/my_agent.py:

from __future__ import annotations

import json
from datetime import datetime, timezone

import anthropic
from mcp import ClientSession

from morning_agents.agents.base import BaseAgent
from morning_agents.config import MODEL
from morning_agents.contracts.models import AgentResult, AgentStatus, Finding, Severity, ToolCall
from morning_agents.skills.mcp_utils import call_tool, parse_tool_result, strip_fences
from morning_agents.skills.timing import elapsed_ms, ms_timer

_client = anthropic.AsyncAnthropic()


class MyAgent(BaseAgent):
    name = "my_agent"
    display_name = "🔥 My Agent"
    mcp_servers = ["some-mcp"]          # must match SERVER_REGISTRY keys
    depends_on = []                     # optional: list of agent names to wait for
    workspace_type = "none"             # "none" | "scratch" | "persistent"

    def get_system_prompt(self) -> str:
        return (
            "You are a specialist. "
            "Always respond with valid JSON matching this shape:\n"
            '{"findings": [{"title": str, "detail": str, "severity": "info"|"warning"|"action_needed"}]}'
        )

    async def run(self, sessions: dict[str, ClientSession], upstream: dict | None = None) -> AgentResult:
        started_at = datetime.now(tz=timezone.utc)
        session = sessions["some-mcp"]
        tool_calls: list[ToolCall] = []
        findings: list[Finding] = []

        # 1. Call MCP tools
        with ms_timer() as elapsed:
            result = await call_tool(session, "some_tool", {})
        tool_calls.append(ToolCall(tool="some_tool", server="some-mcp", duration_ms=elapsed[0], success=True))

        data = parse_tool_result(result)

        # 2. Feed to Claude
        with ms_timer() as elapsed:
            response = await _client.messages.create(
                model=MODEL,
                max_tokens=1024,
                system=self.get_system_prompt(),
                messages=[{"role": "user", "content": json.dumps(data)}],
            )
        tool_calls.append(ToolCall(tool="messages.create", server="anthropic", duration_ms=elapsed[0], success=True))

        # 3. Parse findings
        try:
            parsed = json.loads(strip_fences(response.content[0].text))
        except json.JSONDecodeError:
            completed_at = datetime.now(tz=timezone.utc)
            return AgentResult(
                agent_name=self.name,
                agent_display_name=self.display_name,
                status=AgentStatus.error,
                started_at=started_at,
                completed_at=completed_at,
                duration_ms=elapsed_ms(started_at, completed_at),
                tool_calls=tool_calls,
                error="Failed to parse Claude response",
            )

        now = datetime.now(tz=timezone.utc)

        for i, item in enumerate(parsed.get("findings", []), start=1):
            findings.append(Finding(
                id=f"my_agent-{i:03d}",
                source_agent=self.name,
                category="some_category",
                severity=Severity(item.get("severity", "info")),
                title=item.get("title", "?"),
                detail=item.get("detail", ""),
                metadata={"tool_id": "my_tool_id"},   # always include tool_id
                timestamp=now,
            ))

        if not findings:
            findings.append(Finding(
                id="my_agent-000",
                source_agent=self.name,
                category="all_clear",
                severity=Severity.info,
                title="Everything looks good",
                detail="No issues found.",
                metadata={"tool_id": "my_tool_id"},
                timestamp=now,
            ))

        completed_at = datetime.now(tz=timezone.utc)
        return AgentResult(
            agent_name=self.name,
            agent_display_name=self.display_name,
            status=AgentStatus.success,
            started_at=started_at,
            completed_at=completed_at,
            duration_ms=elapsed_ms(started_at, completed_at),
            findings=findings,
            tool_calls=tool_calls,
        )

Step 2: Register the MCP Server

In morning_agents/config.py, add to SERVER_REGISTRY:

SERVER_REGISTRY = {
    # ... existing entries ...
    "some-mcp": StdioServerParameters(
        command="bun",
        args=["run", str(Path(__file__).parent.parent / "mcp-servers" / "some-mcp" / "index.ts")],
    ),
}

Step 3: Register the Agent in the CLI

In morning_agents/cli.py:

from morning_agents.agents.my_agent import MyAgent

_AGENTS = {
    "brewmaster": BrewmasterAgent,
    "devenv": DevEnvAgent,
    "pr_queue": PRQueueAgent,
    "my_agent": MyAgent,    # add this
}

Step 4: Use tool_id Consistently

The tool_id field in Finding.metadata is how cross-reference rules identify findings. Pick a stable, lowercase identifier and use it on every finding your agent emits.

metadata={"tool_id": "my_tool_id", ...}

Step 5: Write Evals

Add evals/test_my_agent.py following the pattern in evals/test_brewmaster.py. Unit tests (no network) go in evals/test_*.py and run with:

uv run pytest evals/ -v