Skip to main content
Glama

🩺 IncidentIQ

An AI on-call engineer that actually checks its work before it blames anyone.


The 30-second pitch

Imagine payments on your site just started failing. You'd want an on-call engineer who does two things at once: checks what's actually happening right now (is the database okay? did someone deploy something?), and remembers what happened last time something like this occurred β€” without blindly assuming this time is the same.

IncidentIQ is that engineer, built as an AI agent. Give it a one-line description of an incident, and it investigates on its own β€” deciding which tool to reach for next based on what it just learned, the same way a real SRE would β€” until it has enough evidence to hand you a root cause, not a guess.

It's a from-scratch build of the two ideas everyone's talking about right now, RAG and MCP, done deliberately slowly over about a week so every layer could be understood and tested on its own before the next one got stacked on top. Not a framework demo. Not a tutorial copy-paste. Six days, six testable layers, one real bug found and fixed by actually running the thing.

  • MCP (Model Context Protocol) is just a standard way to hand an AI agent a set of tools β€” think "check the database," "read the logs" β€” the same way a human on-call engineer would open a few dashboards.

  • RAG (Retrieval-Augmented Generation) is how the agent gets a memory β€” a searchable library of past incident reports it can query for precedent, instead of only knowing what's in its training data.

The interesting part isn't building either one β€” it's realizing the agent shouldn't have to think about them differently. See "How it thinks" below.

Related MCP server: semley

πŸ‘€ See it in action

Real, unedited output β€” not a mockup β€” from asking it:

"Payments started failing around 2:30 PM. What could be the cause?"

πŸ”§ get_service_health(payment-service) β†’ down, error_rate 0.87
πŸ”§ get_service_health(auth/order/database/kafka)  β†’ 4 healthy-or-degraded reads
πŸ”§ get_database_status()          β†’ connection pool 99% exhausted since 14:25
πŸ”§ get_recent_logs(payment-service, since=14:15) β†’ deploy log line found
πŸ”§ get_deployment_info(payment-service) β†’ dep-1045: pool size cut 50 β†’ 10
πŸ”§ get_deployment_info(order-service, database)  β†’ ruling out other causes
πŸ“š search_incident_history("connection pool exhaustion...")
   β†’ finds a *similar-looking* past incident with a DIFFERENT cause

πŸ’¬ Root cause: dep-1045 (14:15:00) cut payment-service's DB connection
   pool from 50 β†’ 10. Undersized pool couldn't serve normal traffic,
   requests queued and timed out, retry storm saturated the database.

   Ruled out: traffic spike (the cause of a similar-looking past
   incident) β€” request volume was flat before AND after the deploy,
   so this isn't that.

   Confidence: High.  β†’  4 iterations, 12 tool calls, 0 errors.

The agent retrieved a past incident that looked almost identical β€” then checked live evidence, decided that incident's explanation didn't apply here, and said so explicitly. That's not an accident; it's the one design decision this whole project is built around. Full trace, full walkthrough, and where the agent could've dug one level deeper: Example run.

⚑ Quick start

git clone <this-repo> && cd IncidentIQ
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

python build_index.py                 # embeds the knowledge base (~90MB model, one-time)
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env

streamlit run app.py                  # the UI, or:
python run_agent.py                   # ...the terminal version

That's it β€” no Docker, no external services, no real infrastructure. Everything the agent investigates (world/) and everything it can research (docs/) is fake, hand-built, and lives in this repo. See Running it for the full breakdown, including how to test each layer on its own.

🧠 How it thinks: the one design decision that matters

To the agent, there is no difference between "check what's happening right now" and "search what happened before." Both are just entries in one list of 6 tools β€” same server, same calling convention, same description style. That's enforced at the code level, not just a nice idea: search_incident_history (RAG) sits in the exact same MCP server as the five live-state tools, registered the same way. If retrieval and live tools lived in separate systems, the agent would have to reason about which subsystem to talk to β€” a distinction that matters to the people building this, but has no business leaking into the agent's own judgment.

But the two kinds of information don't earn the same trust. Live tools report the current, actual truth of the system. A retrieved past incident is only ever a lead β€” because the knowledge base contains a deliberate trap: INC-2024-0417 looks, at the symptom level, identical to the live incident (same "connection pool exhausted" signature) β€” but its real cause was a traffic spike with no deploy at all, the opposite of what's actually happening live. So the system prompt says, explicitly: retrieved history is a hypothesis to check, never a conclusion to repeat β€” a root cause is only stated once live data confirms it. The UI reinforces the same idea visually, tagging every step πŸ”§ (live) or πŸ“š (historical) so the distinction is legible to a human watching, not just buried in a prompt nobody reads.

This got tested for real, not just designed on paper β€” the trap fired in the actual run above, and the agent didn't fall for it. See the walkthrough in Example run.

πŸ—οΈ Architecture

flowchart TB
    subgraph World["world/ β€” fake production state (JSON + logs)"]
        services["services.json"]
        logs["logs/*.log"]
        deploys["deployments.json"]
        dbstatus["database_status.json"]
    end

    subgraph Docs["docs/ β€” knowledge base (markdown)"]
        incidents["3 past incident postmortems"]
        runbook["1 runbook"]
        arch["1 architecture doc"]
        api["1 API reference"]
    end

    subgraph RAGPipeline["rag/ β€” retrieval pipeline"]
        chunker["chunker.py β€” header-aware chunking"]
        store["store.py β€” embed (all-MiniLM-L6-v2) + store (Chroma)"]
    end
    Docs --> chunker --> store

    subgraph MCPServer["server/ β€” one MCP server, 6 tools"]
        data["data.py"]
        liveTools["πŸ”§ get_service_health<br/>πŸ”§ get_recent_logs<br/>πŸ”§ get_kafka_consumer_lag<br/>πŸ”§ get_deployment_info<br/>πŸ”§ get_database_status"]
        ragTool["πŸ“š search_incident_history"]
    end
    World --> data --> liveTools
    store --> ragTool

    subgraph AgentPkg["agent/ β€” orchestration"]
        bridge["mcp_client.py β€” MCP tools to Claude tool format"]
        loop["loop.py β€” manual agentic loop (Claude Opus 5)"]
    end
    liveTools --> bridge
    ragTool --> bridge
    bridge --> loop

    subgraph Presentation["presentation only β€” no investigation logic"]
        cli["run_agent.py (CLI trace)"]
        streamlit["app.py (Streamlit UI)"]
    end
    loop --> cli
    loop --> streamlit

Nothing here is exotic β€” a JSON+log fixture, a vector DB, an MCP server, a while loop calling the Claude API. The interesting part is the order it got built in: every layer had to prove itself before the next one was allowed to depend on it.

Day

Built

Proven by

1

world/ β€” fake services, logs, deploys, DB status

Manual inspection, consistent timestamps across every file

2

server/ β€” MCP server, 5 live-state tools

test_tools.py β€” real MCP protocol, not bare function calls

3

docs/ β€” knowledge base + chunking strategy

chunk_docs.py β€” chunk boundaries, table/code-fence integrity

4

rag/ β€” embeddings + Chroma + search_incident_history

test_retrieval.py β€” retrieval quality graded query by query

5

agent/ β€” the orchestration loop

Code-verified, then a real live run against the Claude API

6

app.py β€” Streamlit UI

Verified live in-browser, including a bug found and fixed mid-test

πŸ“Ό Example run (real trace)

Real captured output from python run_agent.py against the default query, model claude-opus-5, unedited except for trimming a few long tool results (marked [truncated, N chars total] β€” that's the CLI's own behavior, not something removed for this README):

USER QUERY: Payments started failing around 2:30 PM. What could be the cause?
Connected to MCP server - 6 tools available:
  - get_service_health
  - get_recent_logs
  - get_kafka_consumer_lag
  - get_deployment_info
  - get_database_status
  - search_incident_history

==========================================================================================
ITERATION 1
==========================================================================================

πŸ’¬ CLAUDE:
I'll start by checking health across all services to see the full picture.

πŸ”§ TOOL CALL: get_service_health({"service": "payment-service"})
πŸ”§ TOOL RESULT [OK]:
{"name": "payment-service", "status": "down", "error_rate": 0.87, "avg_latency_ms": 9800, ...}

πŸ”§ TOOL CALL: get_service_health({"service": "auth-service"})
πŸ”§ TOOL RESULT [OK]: {"name": "auth-service", "status": "healthy", "error_rate": 0.002, ...}

πŸ”§ TOOL CALL: get_service_health({"service": "order-service"})
πŸ”§ TOOL RESULT [OK]: {"name": "order-service", "status": "degraded", "error_rate": 0.34, ...}

πŸ”§ TOOL CALL: get_service_health({"service": "database"})
πŸ”§ TOOL RESULT [OK]: {"name": "database", "status": "degraded", "error_rate": 0.51, ...}

πŸ”§ TOOL CALL: get_service_health({"service": "kafka"})
πŸ”§ TOOL RESULT [OK]: {"name": "kafka", "status": "healthy", "error_rate": 0.001, ...}

==========================================================================================
ITERATION 2
==========================================================================================

🧠 THINKING:
Payment is down, order and database are degraded while auth and kafka remain
healthy. I should check the DB pool status and logs, along with recent
deployments, to trace the cause.

πŸ’¬ CLAUDE:
Payment-service is down, order-service and database are degraded, auth and
kafka are fine. Let me dig into the database pool and payment logs, plus
recent deploys.

πŸ”§ TOOL CALL: get_database_status({})
πŸ”§ TOOL RESULT [OK]:
{
  "database": "orders-primary-pg", "current_status": "degraded",
  "incident_started_at": "2026-09-05T14:25:00-05:00",
  "connection_pool": {"max_connections": 200, "active_connections": 198,
                       "waiting_requests": 143, "utilization_pct": 99},
  "history": [
    {"timestamp": "...14:15:00...", "note": "payment-service deployed v4.2.1 (pool max reduced 50->10 on client side)"},
    {"timestamp": "...14:23:00...", "note": "payment-service connection pool (max 10) saturated, requests queuing on client side"},
    ...
  ]
}

πŸ”§ TOOL CALL: get_recent_logs({"service": "payment-service", "since": "14:15"})
πŸ”§ TOOL RESULT [OK]: {"count": 155, "logs": [
  {"timestamp": "2026-09-05 14:15:03", "level": "INFO", "message": "Deployment complete: version=v4.2.1 (config: db_pool.max_size 50 -> 10)"},
  ... [truncated, 25820 chars total]

πŸ”§ TOOL CALL: get_deployment_info({"service": "payment-service"})
πŸ”§ TOOL RESULT [OK]:
{"service": "payment-service", "count": 1, "deployments": [
  {"id": "dep-1045", "timestamp": "2026-09-05T14:15:00-05:00", "version": "v4.2.1",
   "change_description": "Reduce DB connection pool max size from 50 to 10 to lower idle connection count per cost-optimization ticket PERF-881",
   "deployed_by": "arjun.kapoor", "status": "success"}
]}

==========================================================================================
ITERATION 3
==========================================================================================

🧠 THINKING:
This is compelling but I should rule out alternative explanations before
concluding. I'll check order-service and database deployment history, and
search for similar past incidents, to make sure I'm not missing a competing
cause like a traffic spike.

πŸ”§ TOOL CALL: get_deployment_info({"service": "order-service"})
πŸ”§ TOOL RESULT [OK]: {"count": 1, "deployments": [{"id": "dep-1042", "timestamp": "2026-09-03T16:40:00-05:00", ...}]}

πŸ”§ TOOL CALL: get_deployment_info({"service": "database"})
πŸ”§ TOOL RESULT [OK]: {"count": 1, "deployments": [{"id": "dep-1044", "timestamp": "2026-09-05T06:00:00-05:00", ...}]}

πŸ“š TOOL CALL: search_incident_history({"query": "payment-service database connection pool exhaustion connection timeout"})
πŸ“š TOOL RESULT [OK]:
{"count": 5, "results": [
  {"similarity": 0.694, "source": "architecture/payment-service-database-kafka-architecture.md",
   "section": "... > payment-service ↔ database > Connection pooling",
   "content": "Both present identically at the symptom level - `connection timeout to database` errors ... so distinguishing them requires checking deployment history *and* traffic volume, not just the pool metrics alone."},
  {"similarity": 0.652, "source": "incidents/INC-2024-0417-payment-service-db-pool-exhaustion.md",
   "section": "... > Lessons learned",
   "content": "We correctly avoided the trap of assuming \"connection timeout to database\" always means a code/config regression ..."},
  ... [truncated, 5536 chars total]

πŸ”§ TOOL CALL: get_recent_logs({"service": "database", "since": "14:20"})
πŸ”§ TOOL RESULT [OK]: {"count": 41, "logs": [
  {"timestamp": "2026-09-05 14:20:00", "level": "WARN", "message": "orders-primary-pg connection pool utilization rising, active_connections=89"},
  ...
  {"timestamp": "2026-09-05 14:25:00", "level": "ERROR", "message": "orders-primary-pg connection pool exhausted: 160/200 connections active, 77 requests waiting"},
  ... [truncated, 7858 chars total]

==========================================================================================
ITERATION 4
==========================================================================================

🧠 THINKING:
Checking request cadence in the logs, traffic volume stayed steady before
and after 14:15, ruling out a traffic spike as the cause. I also confirm
the order-service retry deploy from Sep 3 amplified the issue. That's
enough evidence to move forward.

==========================================================================================
FINAL ANSWER (after 4 iteration(s), 12 tool call(s))
==========================================================================================
## Root cause

**A bad config change in payment-service deploy `dep-1045` (v4.2.1,
2026-09-05 14:15:00 CDT) that cut the client-side DB connection pool from
`max_size=50` to `max_size=10`.** The undersized pool couldn't serve normal
traffic, requests queued and timed out, and the retry storm then saturated
`orders-primary-pg`, dragging order-service down with it.

## Evidence (live data)

**The trigger β€” deploy at 14:15:**
- `dep-1045`, v4.2.1, deployed 14:15:00 by arjun.kapoor: "Reduce DB
  connection pool max size from 50 to 10 to lower idle connection count
  per cost-optimization ticket PERF-881" β€” status: success.
- payment-service log 14:15:03: `Deployment complete: version=v4.2.1
  (config: db_pool.max_size 50 -> 10)`.

**The causal chain, timestamp by timestamp:**
- 14:00–14:15 β€” healthy, 62–65 active connections, 0 waiting, 31–33% utilization.
- 14:20 β€” 71 active, 3 waiting: "Slight uptick in queued acquisitions from payment-service clients."
- 14:23 β€” 88 active, 19 waiting: pool "saturated, requests queuing on client side."
- 14:25 β€” 160 active, 77 waiting: clients "opening redundant retry connections."
- 14:40 (now) β€” 198/200 active, 143 waiting, 99% utilization, "no mitigation applied yet."

**Ruled out alternatives:**
- **Traffic spike** (the root cause of the very similar past incident
  INC-2024-0417, flash sale at 14:02): *not supported here.*
  Payment-service request cadence in the logs is flat from 14:15 onward β€”
  no volume increase. DB active connections were steady right up to the
  14:15 deploy. The rise starts at the deploy, not before it.
- **Other deploys:** order-service's only deploy is 2 days earlier;
  database's was a routine patch 8+ hours before onset. Neither correlates.
- **Auth/Kafka:** both healthy β€” not involved.

## Confidence

**High.** The deploy description, the payment-service log line, the error
message literally quoting `max_size=10`, and the connection-pool timeline
all independently converge on the same 14:15 config change.

## Suggested next steps

1. Roll back `dep-1045` / restore `db_pool.max_size` to 50.
2. Don't just restart payment-service β€” INC-2024-0417's lesson applies:
   restarts fix leaked connections, not undersized pools.
3. Consider briefly throttling order-service retries while the DB drains
   the 143-request backlog.
4. Follow up on PERF-881 with a pool size validated against actual
   concurrency, not a flat 5x reduction.

Walkthrough β€” what the agent did, and where it fell short

Iteration 1 was pure triage: all 5 get_service_health calls in parallel, no assumptions. Iteration 2 fired 3 more calls in parallel (get_database_status, get_recent_logs, get_deployment_info) β€” notably, the since="14:15" window was chosen as a reasonable generic buffer before any of that batch's results came back (all three were decided in one turn), and it happened to land exactly on the real deploy time. Iteration 3 made the single search_incident_history call of the whole run, with a well-composed multi-concept query, then explicitly worked to rule out order-service's and database's own deploys and check whether traffic volume (not just a deploy) could explain it.

The moment I was watching for most closely: the RAG search's second result was INC-2024-0417 β€” the deliberately-planted trap incident with a different (traffic-spike) root cause. The final answer didn't just retrieve it; it explicitly checked live evidence against it and rejected it ("Payments-service request cadence in the logs is flat... not supported here"). That's the RAG-grounding policy working exactly as designed, several days after it was only a design decision on paper.

Where it fell short: order-service showed degraded (0.34 error rate) in iteration 1, but the agent never called get_recent_logs against order-service itself to verify it β€” it inferred order-service's involvement entirely from a note embedded in the database's status history ("order-service secondary read queries also beginning to queue"). That note is accurate, so the conclusion wasn't wrong, but a more thorough investigation would have pulled order-service's own logs and seen the actual cascading 502/timeout pattern directly rather than taking a secondhand mention as sufficient. The one clear "go one level deeper" moment in an otherwise clean run.

Totals: 4 iterations, 12 tool calls, 0 errors, 0 duplicate calls, 0 fallbacks/refusals/truncation β€” well inside the 12-iteration budget.

πŸ•ΉοΈ Running it

python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

# Build the fake world's search index (only needed once, or after editing docs/)
python build_index.py

# CLI, full trace printed to the terminal
export ANTHROPIC_API_KEY=sk-ant-...
python run_agent.py "Payments started failing around 2:30 PM. What could be the cause?"

# Streamlit UI (also loads ANTHROPIC_API_KEY from a local .env if present,
# or you can paste a key directly into the page)
streamlit run app.py

Individual pieces can be exercised in isolation too β€” useful for trusting each layer before trusting the whole:

python test_tools.py       # all 6 MCP tools, real protocol, no LLM involved
python test_retrieval.py   # RAG retrieval quality, graded query by query

⚠️ Known limitations

Being upfront about these rather than presenting this as production-ready:

  • It's entirely fake infrastructure. world/ and docs/ are hand-authored fixtures, not real telemetry or a real incident corpus. Nothing here has been validated against a real production system.

  • The retrieval "trap" safety net is thinner than it looks. As described above, the disambiguating "this was not a deploy" sentence in INC-2024-0417 ranks outside top-5 for the queries most likely to need it (rank 8, similarity 0.410, for "database connection pool exhausted"; absent from the top 10 entirely for "payment service errors after a deploy"). The system currently relies on (a) the architecture doc independently repeating similar disambiguating language, which happens to rank higher, and (b) the agent's system-prompt instruction to verify against live tools β€” not on retrieval ranking reliably surfacing the right counter-evidence on its own. If either the architecture doc's wording or the prompt instruction were removed, this could break silently.

  • The embedding model doesn't understand negation. A chunk that narrates "checked deployment history... no deploys in the last 24h" still scores as a strong match for a query like "payment service errors after a deploy" β€” the embedding model matches on topical similarity, not on whether the passage confirms or rules out what's being asked.

  • all-MiniLM-L6-v2 is a symmetric similarity model, not tuned for asymmetric queryβ†’passage retrieval. Imperative, question-style queries ("how do I roll back a bad deploy") return much lower absolute similarity scores (~0.22-0.33) than descriptive queries (~0.5-0.7) even when the correct document still ranks first β€” so a similarity-threshold cutoff for "no good match found" would not be well-calibrated across different query phrasings.

  • A few chunks intentionally exceed the target chunk size. 3 of 54 chunks (incident report "Timeline" sections) are oversized because a bulleted list with no internal blank lines is one atomic block by design β€” kept whole on purpose, since splitting a timeline mid-sequence would lose the causal narrative an incident investigation actually needs, at the cost of a larger-than-target chunk.

  • max_iterations=12 is a hard cap on the agent loop with no smarter fallback β€” a genuinely hard investigation could be cut off before reaching a conclusion. The UI/CLI say so explicitly rather than silently truncating, but there's no partial-answer synthesis.

  • No memory across investigations. Every query starts a fresh loop; nothing is remembered between runs or across a Streamlit page reload.

  • No systematic eval. Retrieval and tool behavior were spot-checked against a handful of hand-picked queries (test_retrieval.py), not measured against a proper labeled eval set.

  • Streamlit app is single-user, local-only, no auth, no persistence β€” a demo shell, not a deployable multi-user tool.

πŸŽ“ What building this actually taught me

  • Tool descriptions are the real interface. An LLM never sees your code, your comments, or your intentions β€” only the name, description, and JSON schema you hand it. Half of this project's actual engineering was writing descriptions precise enough that a model reaching for the wrong tool became the exception, not the norm.

  • Retrieval quality has to be evaluated adversarially, not just happily. It's easy to build a knowledge base where every test query finds a good match. It's much more informative to plant a trap β€” a document that's similar but wrong β€” and see whether the system (prompt + retrieval + agent judgment together) actually catches it. Ours did, but only barely: the safety net (see Known limitations) is thinner than the demo suggests.

  • A live run finds bugs a synthetic test can't. The Streamlit final card looked perfect with hand-written sample data β€” headers, bold, bullets all styled correctly. Then a real model response, full of actual ## and ** markdown, exposed that st.html() doesn't parse markdown at all. Nothing about that would have shown up without actually running the real thing end to end.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables autonomous SRE incident investigation by allowing users to describe incidents in natural language. The agent follows a governed state machine to gather read-only evidence and produce grounded conclusions.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables autonomous infrastructure health management by exposing tools for retrieving system logs, querying a knowledge base, executing SQL analytics, and simulating system commands, all integrated into an AI-driven incident response workflow.
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables users to investigate infrastructure incidents in plain English, correlate observability and deploy data with runbooks, and get evidence-backed root-cause proposals with approval-gated remediation.
    5
    -