top of page
Search

RunReveal Notifications & the MCP Server I Built

10 minutes ago
8 min read


Earlier, I described the escalation ladder — detection, then signal, then alert — and noted that the last step only happens once a notification channel is attached.

This is that channel layer.

RunReveal supports twelve destinations for alert output: Email, Slack via webhook, Slack via a dedicated bot integration, Discord, PagerDuty, VictorOps, Jira, Google Chat, Linear, incident.io, generic Webhooks, and Tines.

That's a reasonable spread — enough to plug into most on-call and ticketing stacks without forcing you through a generic webhook for everything, which is where a lot of smaller SIEM offerings stop.


Setting One Up

Creating a channel is a one-time step; attaching it to a detection is what actually turns the tap on. A detection or agent with no channel attached can run forever, match real data, and never bother a single person — which is exactly the safe state you want while you're still validating that something works correctly.


A Lesson I Had to Learn Twice

I want to be honest about something rather than write around it, because it's the most useful thing in this article. Earlier in this series' research, testing a detection end-to-end with a real notification channel attached resulted in an actual email going out to a real person in the workspace — the detection matched a near-guaranteed-to-fire condition faster than I expected, and the channel was live.

Later, , the same category of mistake nearly happened again from a different angle: creating an agent through the API with the notification field left unset caused it to default to the same email channel automatically, rather than staying unattached.


The actual rule I now follow: Never attach a real notification channel to anything — a detection or an agent — until you have watched it complete at least one full, safe test cycle and read the output yourself. Assume any new object might default to a live channel unless you have explicitly confirmed otherwise by reading the object back, not just by reading what you sent.

It's a small operational discipline, but it's the difference between testing safely and accidentally paging a real colleague over something you were only trying to verify works.


Keep it in mind for the second half of this article, because it ended up shaping how I built my own MCP server.

The MCP Server I Built

Most of the hands-on findings came from a conversation. I'd ask a question in MCP it would run SQL, I'd read the result, and we'd go again.


What made that possible is a small MCP server I wrote against RunReveal's REST API.

This half of the article is about that server: what's in it, what I deliberately kept out of it, and a few real sessions that show why I think it's the most useful thing I built during this whole series.


If you haven't met MCP yet:

the Model Context Protocol is a standard way to hand an AI assistant a set of tools. The assistant sees each tool's name, description and parameters, decides when to call one, and gets the JSON result back. The server is the bit that sits between the assistant and your real system, and it decides exactly what the assistant is allowed to touch.



What it is

It's about 330 lines of Python. A thin HTTP client built on httpx handles auth and error handling, a FastMCP server defines the tools, and a tiny CLI picks the transport. It runs over stdio for AI tool, or over SSE on a local port if you want to point something like Bedrock at it. Wiring it into Claude Code is one JSON block:

{
  "mcpServers": {
    "runreveal": {
      "command": "uv",
      "args": ["--directory", "/path/to/runreveal-mcp", "run", "runreveal-mcp"]
    }
  }
}

Auth was the first thing the docs got wrong.

The example request on RunReveal's auth page 404s even with a valid token.

What actually works is HTTP Basic auth where the token from the dashboard (Workspace → API Tokens) is already the finished credential: you send Authorization: Basic <token> as-is, with no extra base64 step. I only found that by trial and error against the live API.

It exposes eleven tools, split by how much damage they can do:

  • Read (7): query_logs, list_sources, list_detections, get_detection, list_notifications, list_investigations, get_investigation

  • Write (3): create_investigation, add_investigation_note, add_investigation_indicator

  • Correlate (1): ioc_lookup, which searches every case in the workspace for a prior indicator match


Every one of those wraps an endpoint I called by hand first and checked the response of. None were written from the docs alone.



RunReveal already has an MCP server. Why build another?


Fair question.

RunReveal ships an official one, and it's good. There's a hosted version at api.runreveal.com/mcp that uses OAuth, and a local version you run with the runreveal CLI using an API token.

It exposes a lot: querying and schema tools, but also detections_create, detection_update, detection_delete, sigma_create, agents_create, agents_delete, agents_silence, investigation_create, dashboard_graph_create and notification_send.


Read that last list again with the first half of this article in mind. I had already sent one real email to a real colleague by accident, and nearly sent a second through an agent that switched itself on. Handing an AI model a tool called notification_send, in a workspace where the only channel is a live email address, is the same mistake with the model making it instead of me. Same for agents_create, given that RunReveal's own create endpoint ignores enabled: false


So what i built is the other way around. The model can read anything it needs to investigate, and the only things it can write are case notes and indicators, which are append-only and land in a case someone has to look at anyway. It cannot create or change a detection, start an agent, touch a pipeline, delete anything, or send a notification. The worst a confused model can do with my server is open a case with a bad title.


The design rule: Give the model every read it needs and almost no writes. A read that goes wrong wastes a query. A write that goes wrong can page someone, silence a detection, or delete evidence.

What I left out on purpose

Beyond the safety choice above, some tools are missing because the endpoints behind them don't work. Detection delete, pipeline add-step and execute-step, and investigation close/update/delete were all broken or unverified server-side when I am creating this article. I'd rather ship eleven tools that work than fourteen where three fail in ways the model can't explain back to me. The list is written into the server's docstring so the next person doesn't add them without testing.


Gotchas baked into the wrappers

Most of the value of a wrapper is the stuff it quietly gets right so the model doesn't have to. Each of these cost me at least one failed call to find:


  • Errors come back as HTTP 200. RunReveal wraps every response in {success, result}, and a failed call can still be a 200 with success: false. The client checks that flag and raises with the API's own error text, because a bare status line tells you nothing.

  • Optional parameters must be dropped, not passed through. A Python None handed straight to the query string risks going out as the literal text "None", which the API would treat as a real filter value. The client strips None values before every request.

  • Artifact types are lowercase. Adding a note to a case needs type "note", and indicators need "indicators". The console labels them "Note" and "Indicators", and the capitalised versions fail.

  • Detections are fetched by name, not ID (?name=), while investigations and agents are fetched by ID (?id=). Same API, opposite conventions.

  • Map columns like actor and tags need bracket syntax in SQL (actor['email'], not actor.email). That hint lives in the query_logs description so the model gets it right first time.


What It Looks Like in Use: Three Real Sessions

This is the part that sold me on it. Each of these is a real exchange against the live workspace. I've trimmed the output, but not changed it.


Session 1: "Will this detection ever fire?"

A test detection for failed Google Workspace admin logins had run every five minutes for over a week with zero hits, and I asked whether that meant nothing had happened.


The model pulled the detection with get_detection, saw the filter was eventName ILIKE '%fail%login%', and instead of answering it checked what event names the source actually produces:

-- query_logs
SELECT DISTINCT eventName FROM logs WHERE sourceType = 'google-workspace-alerts'

Admin password reset
Suspicious login
Activity Rule

None of those can ever match that filter, so the rule is structurally dead, not quiet. That took two tool calls inside one conversation. In the console it's a detection edit screen, an Explorer tab and a manual comparison, which is exactly the kind of check that's easy to skip.


Session 2: "46 admin password resets in an hour"

A burst-of-password-resets detection returns 46 resets in 60 minutes, with an empty actor. That looks like a mass account takeover. The model's first move was to regroup the same rows by the alert's own ID, pulled out of the raw log, instead of by actor. I re-ran it today while writing this, to see whether anything had changed:

-- query_logs, whole source
SELECT count() AS raw_rows,
       uniqExact(JSONExtractString(rawLog,'alertId')) AS distinct_alerts,
       countIf(actor['email'] = '') AS empty_actor_rows
FROM logs WHERE sourceType = 'google-workspace-alerts'

raw_rows: 90997    distinct_alerts: 4    empty_actor_rows: 90997

-- same source, last three hours, per hour
hour 05:00   rows 45   distinct_alerts 1
hour 06:00   rows 46   distinct_alerts 1
hour 07:00   rows 45   distinct_alerts 1

When I wrote article the source had 86,661 rows. Four days later it has 90,997, still only four real Google alerts, and still the same one alert re-ingested about 45 times an hour. The actor field is empty on every single row, because this connector only puts the email inside rawLog. So the "burst" is a polling artifact, and the growth since previous article is the bug still running. Checking that took one follow-up question, not a new investigation.


Session 3: working the case, and correlation across cases

Once the triage was done, the model opened a case with create_investigation, wrote the reasoning into it with add_investigation_note, and attached the admin email as a structured indicator with add_investigation_indicator. Then it ran ioc_lookup on that email to see whether it had come up before:

-- ioc_lookup(ioc_type='email', ioc_value='admin@')
Burst of admin password resets (GWS alerts)   closed  false_positive  high
Demo: Admin password reset - admin@<lab>      closed  false_positive  high

-- ioc_lookup(ioc_type='Email', ioc_value='admin@')
null

Two things there.

  • First, correlation works: the new case immediately linked back to an older closed case about the same account, also marked false positive, which is exactly the context you want before escalating.

  • Second, the value match is partial and case-insensitive, but the type match is exact and case-sensitive. Ask for "Email" and you get nothing back, with no error. The tool description spells out that the type is an exact match, so the model uses the lowercase form.


One detail matters here: notes are not searchable by ioc_lookup, only indicators are. If you write "the attacker IP was 1.2.3.4" in a note, the next case will never find it. That's why the note and indicator tools are separate and the note tool's description says so.

Why this is the part I care about: Question → SQL → evidence → case → correlation, in one conversation, without leaving the terminal. And at no point could the model silence a detection, delete anything, or email anyone. That combination of a short loop and a hard safety boundary is what makes it something I'd actually use on a real incident.


And RunReveal's Own AI Chat?

RunReveal also has native AI Chat inside the console, plus the Agents.


Both are bring-your-own-model: under Settings → AI Settings you add Anthropic, OpenAI or Google AI with an API key, Amazon Bedrock with an IAM role RunReveal assumes (role ARN, region, optional external ID), or Vertex AI with a service account. There's no built-in default model.


I haven't connected a model provider to this workspace, so I'm not going to describe what the chat is like to use from the docs. Part of that is deliberate. Connecting a provider means the workspace's logs flow to that provider under whoever's key or role it is, and that's a decision for whoever owns the data and the AI account, not something to do casually for a blog post.


The MCP route sidesteps that: the model I already use calls the API with a scoped token, and I decide tool by tool what it can reach.

--------------------------------------------------Dean--------------------------------------------

Next Part pulls back to the bigger picture — how RunReveal actually stacks up against Splunk and Microsoft Sentinel for a working SOC analyst.

 
 
 

Comments


Ready to discuss:

- Schedule a call for a consultation

- Message me via "Let's Chat" for quick questions

​

Let's connect!

Subscribe to our newsletter

Connect With Me:

  • LinkedIn
  • Medium

© 2023 by Cyberengage. All rights reserved.

bottom of page