MCP Server

MCP Server turns RedAmon into an MCP server, so an AI agent you control can connect into RedAmon and drive recon on your behalf. It could be your own Claude Code session, an internal agent, or a nightly script: anything that speaks the Model Context Protocol. With an access token it can list your projects, start a full recon pipeline, ask the attack-surface graph questions in plain English, and adjust recon tuning. It acts as you, inside your own projects, limited to the permissions you tick.

Two MCP tabs, opposite directions. MCP Tool Plugins is outbound: RedAmon's agent connecting out to tools you register. MCP Server is inbound: other agents connecting in to RedAmon. Same protocol, opposite roles.

Looking for the technical deep dive? See docs/readmes/README.MCP.SERVER.md for the security model, the settings allowlist and the nginx wiring.

MCP Server tab in Global Settings: three tokens and the permissions each one holds, with one row's actions menu open on Onboard, Edit and Delete


Why you might want this

RedAmon's own agent is good at driving an engagement from inside the product. But plenty of work happens outside it: a nightly job that rescans a portfolio, a triage assistant that already has your ticketing context, a research agent you have taught your own methodology. Those agents have everything they need except the recon data, and no safe way to ask for it.

A token gives them a narrow, revocable way in. They ask "what did the last scan find on this domain" in plain English and get rows back. They start a scan when something changes. They never see another user's data, never touch your credentials, and never change what RedAmon is pointed at.


How it works, in one picture

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1f2937','primaryTextColor':'#f9fafb','primaryBorderColor':'#ef4444','lineColor':'#9ca3af','textColor':'#f9fafb','edgeLabelBackground':'#1f2937','secondaryColor':'#111827','tertiaryColor':'#111827','noteTextColor':'#111827'}}}%%
flowchart LR
    A[Your AI agent] -->|HTTPS + Bearer token| B["/api/mcp-server<br/>(webapp)"]
    B -->|1. token, permission,<br/>project ownership, rate limit| B
    B -->|scans, settings| C[Recon orchestrator]
    B -->|graph reads| D["Agent /graph/*<br/>read-only, tenant-scoped"]
    D --> E[(Neo4j)]
    D -->|rows| B
    B -->|2. every row re-checked<br/>against your tenant| A
  • Every call is stateless. The token is checked again on every single call, so a revoke or an expiry takes effect on the agent's next call, not its next reconnect.
  • The server never trusts who the caller says it is. The token resolves to exactly one user, and that identity is what reaches the graph.
  • Graph reads never talk to Neo4j directly from the webapp. They go through the agent, which rewrites every query so it can only match your project's nodes, and runs it in a read-only database session.

The thirty-four tools

ToolWhat the agent getsNeeds
list_projectsYour projects (id, name, targets). The only way it discovers a project id, and it cannot see anyone else's.recon:read
get_recon_statusWhether a scan is running, its phase, start and end time, and whether it failed.recon:read
get_recon_settingsThe tuning values it is allowed to change, plus the project's updatedAt for safe writes.recon:read
graph_summaryA count per node type and per relationship type, plus whether the graph is settled right now.recon:read
graph_schemaWhat the graph means: node types, properties, relationships. Reads no data.recon:read
query_graph"Which subdomains have an open admin panel?" Ask in English; it handles the schema.recon:read (+ graph:cypher for raw Cypher)
kali_toolboxWhat the Kali sandbox carries, by category. A list, not a licence: nothing here runs it.recon:read
start_reconStart the full recon pipeline.recon:scan (+ recon:overwrite for overwrite mode)
stop_reconStop the full recon scan running on the project.recon:scan
update_recon_settingsChange recon tuning (rate limits, threads, timeouts, which phases run) AND the engagement's own limits.recon:settings
kali_execA shell in the Kali sandbox: bash -c, full toolset, no target check.kali:exec
kali_outputRead a running command's output, paged from a byte cursor.kali:exec
kali_cancelStop a command it started.kali:exec
list_findingsWhat the scans found, in one ordered list across all eight finding types. Says when nothing is ranked yet.recon:read
list_muted_findingsThe findings someone suppressed as noise. Hidden from every other tool, which is why this exists.triage:read
list_remediationsWhat to fix: priority, CVSS, CVE/CWE/CAPEC, exploit availability, CISA KEV, fix complexity.triage:read
get_project_activityEverything running on the project right now, and whether a scan start would be refused.recon:read
list_scan_versionsThe saved graph versions, and which of them will survive retention.recon:read
compare_scan_versionsWhat changed between two versions of the attack surface.recon:read
describe_recon_settingsEvery settable field, what it means and its real bounds. Reads no data.recon:read
list_recon_presetsThe 26 engagement presets, and how much of each can be applied from here.recon:read
get_attack_surface_overviewOne picture of what the project exposes, from hosts to findings by severity.recon:read
list_exploit_pathsVulnerable technology paired with CVE, ranked by observed exploit then CVSS.recon:read
get_blast_radiusWhich single technology touches the most of the surface. The highest-leverage fix.recon:read
list_graph_viewsThe graph queries a person already saved, by name.recon:read
run_graph_viewRun one of them. No AI call, no budget spent, same answer every time.recon:read + graph:cypher
queue_reconQueue a full recon for when the host is free, instead of being refused.recon:queue
cancel_queued_scanCancel a job it queued, before it starts.recon:queue
get_scan_statusWhether one of the other six scanners is running: GVM, GitHub hunt, supply chain, TruffleHog, AI attack surface, partial recon.recon:read
set_finding_verdictMark a finding confirmed, likely noise, or unreviewed. Durable; survives re-scans.triage:write
create_projectOpen a NEW engagement: its targeting mode, its settings and the record of what authorized it, written atomically. Scope is fixed here and nowhere else.project:create
attach_engagement_authorizationRecord the scope document that permits this engagement: its digest, kind, source and issue date. Append-only.engagement:authorize
list_engagement_authorizationsRead that history: what authorized the engagement, when, and which token recorded it.recon:read
preflight_scope_checkRead-only proof that the configured pipeline fits the engagement. Reports RESOLVED values, not written ones.recon:read

Full API reference: every argument, type, constraint, example call and JSON Schema is on the MCP API Reference page. It is generated from the server itself, so if it ever disagrees with the table above, the reference is right. See Regenerating the API reference.

Not exposed, deliberately: the agent chat, partial recon, deleting projects, your API keys, guardrails, the engagement record (the client, the contacts, the dates, the uploaded document), starting the other scanners (GVM, TruffleHog, supply chain, AI attack surface), captured HTTP traffic, activating or deleting a saved version, and any write to the graph.

Scope is a special case, and the rule is stricter than "not exposed". An agent with project:create can open a NEW engagement and set its targeting mode there — exactly one of targetDomain, targetIps or domainBatchHosts. After that the scope is immutable through every route on this surface: update_recon_settings refuses a targeting column by name, and refuses it by classification rather than by a list, so a targeting column added tomorrow is refused the day it is added. A different target means a different project, which is why create_project exists rather than a way to re-point an existing one. This matters because the documents an agent reads are attacker-influenceable: a target can hand you a "scope document" whose appendix asks for three more hosts.

The engagement's limits are a deliberate exception and go the other way: the rate ceiling, the never-touch hosts, the scanning window and the agent's denylists are ordinary settings here, reachable through update_recon_settings, in either direction. See Rules of Engagement for why a one-direction write rule was the wrong control — and what replaced it.

Their findings are readable (a finding is a finding whichever scanner wrote it) and so is their status, but starting those scans is not. An agent can also record a verdict on a finding, which ranks it. It can never mute or unmute one, which hides or reveals it. Muting is the single action that makes a finding invisible to every other tool here, and unmuting reverses a decision a person made: precisely the power RedAmon's own AI triage is not allowed to have either.

kali_exec is a shell, and it is the exception to everything above. Every other tool here is read-only and tenant-scoped. That one is bash -c with the sandbox's whole toolset and no check on what you point it at, exactly like the in-app agent. It is off by default at three independent levels, and the third cannot be switched on over MCP.

The tool descriptions teach the agent a working order: graph_summary first, then query_graph, and graph_schema only when a query returns nothing or something surprising.

graph_summary: telling "clean" apart from "never scanned"

If an agent asks "list malicious dependencies" and gets nothing back, two very different things could be true: the supply-chain scan ran and your project is clean, or it never ran at all. An agent that cannot tell them apart writes "no malicious dependencies found", which is a false negative in a security tool.

graph_summary settles it. If the node type is missing entirely, that surface was never scanned. It also reports a liveGraphState:

StateMeaning
stableNothing is rewriting the graph. The counts are trustworthy.
scan_runningA scan is writing the graph. Counts can be mid-rebuild. This covers all seven scan kinds: full and partial recon, GVM, GitHub Secret Hunt, TruffleHog, supply chain and AI attack surface.
agent_writingA triage run or an in-app agent session is writing the graph.
activatingA saved version is being swapped in. Counts can be near zero for a moment.
unknownRedAmon could not determine whether anything is writing. The counts cannot be trusted, and this is never reported as stable.

When the state is not stable, the answer carries a warning telling the agent to check again later. Counts are counts only, never sample values, and they skip findings you have muted and findings a later scan no longer reports.

The answer also carries hiddenFromCounts.stale: how many findings were left out because a later scan stopped reporting them. That number is what reconciles a graph_summary count with a query_graph count, which includes stale findings by default. If the figure cannot be read the key is absent, never 0, because a fabricated zero would be the same false negative one field down. Muted findings have no equivalent count here; list_muted_findings is where that number lives.

query_graph: a question or raw Cypher

Send exactly one of question or cypher.

  • question (the default): RedAmon's own LLM turns it into a query using your configured AI model and API key. The answer includes the cypher it generated, so the agent can see what its question became. If generation fails, the error says to rephrase; if running the query fails, it says to narrow the query.
  • cypher: the agent writes the query itself. No LLM runs and no budget is spent. This needs the graph:cypher permission. See Raw Cypher below.

Either way the answer stops at 1,000 rows and then says so with truncated: true. An answer over 2 MB is refused with "narrow your query". Neither is ever cut silently, so a partial answer cannot pass for a complete one. A question can take up to 2 minutes; a raw Cypher call gives up after 1 minute.

Everything the graph returns is data about a live target. Page titles, headers, JS comments and certificate fields are written by whoever runs that site. The tools tell the agent to treat this text as data, never as instructions. The permissions below are what actually stop a manipulated agent from doing damage.

kali_toolbox: what the sandbox carries

RedAmon's Kali sandbox is a specific image, not a stock Kali install. Niche tools an agent assumes are present are frequently absent, and an agent that guesses wrong wastes a call finding out.

kali_toolbox returns the catalogue by category: exploitation, password cracking, web and infrastructure scanning, DNS, Windows and Active Directory, API and GraphQL, secrets, tunnelling, the wordlist paths with their sizes, and the pre-staged post-exploitation toolkits. It is the same catalogue RedAmon's own agent is given, read from one place, so it cannot describe an image the product does not ship.

All of it is runnable. kali_exec is a shell, so anything listed can be run.

It takes no arguments and reads no project data, so it answers even when the Kali sandbox is stopped and when a scan is mid-flight. It costs a read against the cheap rate limit and spends no LLM budget.

kali_exec: a shell in the sandbox

This is the most powerful thing on this surface, and it is deliberately the same access RedAmon's own in-app agent has.

{ "projectId": "<id>", "command": "subfinder -d your-target.tld -silent | httpx -silent -sc | tee /tmp/live.txt" }

It is a real shell. bash -c with the sandbox's full toolset: pipelines, redirection, command substitution, loops and every installed binary. No allowlist. sqlmap, ffuf, nmap --script, hydra, nc and the rest are all available, exactly as they are to the in-app agent.

It waits briefly and returns the output if the command finished. If it is still running you get a jobId: poll kali_output with it, and kali_cancel stops it. Output is paged from a byte cursor, never silently cut.

Nothing checks what you aim at

There is no target check on this path. Not the project's scope, not its engagement limits, not its excluded-host list. If a command names a host, that host is contacted. This is the one place in RedAmon where the scope boundary is not enforced in software.

That is the same position the in-app agent is in — its engagement gate matches tool names and never reads a command string — with one difference that matters: there is no human clicking a confirmation. In the app, a person approves each dangerous tool before it runs. A token has nobody.

Grant kali:exec only to an agent you would trust with a terminal on that box.

Three switches, all of which must be on, each owned by a different person:

#SwitchWho sets itWhere
1MCP_KALI_EXEC_ENABLED=truethe operator, once per install.env
2Shell access to the Kali sandboxyou, password-confirmedthe token's permissions
3Allow MCP Sandbox Commandsa human, per engagementthe project form

Switch 3 cannot be set over MCP. It is denied to update_recon_settings by name, so a token can never grant itself the ability to run commands.

Two limits that are not about safety:

  • 300 seconds per command. The sandbox caps every command, and a broad nuclei -severity info,low,medium or a full testssl will exceed it. A run that hits the cap comes back status: failed with the reason and whatever it printed first — never as a clean result. Split the work rather than having it killed.
  • Output is not streamed. kali_shell returns everything when the process exits, so kali_output reads nothing until the command finishes. Poll with a budget longer than 300 seconds, or a slow command is indistinguishable from a killed one.

/tmp persists between calls, so multi-step work can be staged through files.

Turning it on

The server is controlled by one switch:

# in .env
MCP_SERVER_ENABLED=true

# Off by default even when the line above is on. Only needed for kali_exec.
MCP_KALI_EXEC_ENABLED=false

then docker compose up -d webapp.

  • ./redamon.sh install writes MCP_SERVER_ENABLED=false, so a normal install starts with it off. The repository's .env.example sets it to true for a local stack, so if you built your .env from that file it is already on. That is safe on its own: until someone mints a token, the endpoint answers 401 to everyone.
  • On a server deploy it defaults to off and is refused over plain HTTP (see below).
  • RedAmon refuses to enable it if INTERNAL_API_KEY is missing or still changeme. In that state the agent's own authentication is disabled, so the isolation this feature depends on would not actually be running. ./redamon.sh install generates the secrets.

When the switch is off, the endpoint answers 404 and does no database work at all.


Withdrawing a single tool

If one tool misbehaves you do not have to take the whole surface down. MCP_DISABLED_TOOLS is a comma-separated list of tool names to withdraw:

# in .env, then: docker compose up -d webapp
MCP_DISABLED_TOOLS=compare_scan_versions,queue_recon

A withdrawn tool disappears from the tool list rather than staying visible and refusing, so a connected agent never plans around a tool it cannot use. A name that matches no tool is ignored, because an emergency lever must not be the reason the server fails to start.

Note that the MCP API Reference describes a build, not your deployment: it lists every tool, including any you have withdrawn. Ask the server itself for the authoritative set on one host.

Minting a token

Global Settings → MCP Server → New token.

New access token form: an Agent Profile picker above the permissions, which are grouped into read, run-scans, change-settings and shell-access

  1. Name it for the agent that will hold it (CI nightly rescan, triage assistant). Up to 64 characters.
  2. Pick an Agent Profile - the job this agent is for. Choosing one ticks the permissions that job needs, so you are picking a purpose rather than assembling a permission set. Custom leaves it to you.
  3. Adjust the permissions if you want to. The profile is a starting point, not a lock: tick or untick anything. Two are never ticked for you, whatever the profile - see below.
  4. Pick an expiry. 30, 60 or 90 days, 1 year, or no expiry. Default 90 days.
  5. Confirm your password. Too many wrong attempts lock the form for a while, the same way the login page does.
  6. Copy the token. It is shown once, next to a ready-to-paste client config and an Export onboarding button. Afterwards you only ever see its first 8 characters.

The token is shown once, with the MCP client config beside it

Tokens look like rdmn_mcp_ followed by 48 hex characters. RedAmon stores only a SHA-256 hash of the token, so it cannot show it to you again, and neither can anyone who reads the database.

The permissions

The form groups them the way you should think about them, and this table follows the same order.

Read and query. Nothing here changes any state.

PermissionUI labelLets the agentDefault
recon:readRead recon + graphList projects, read status and settings, query the graph in natural languageon
triage:readRead suppressed findings and remediationsRead the findings a person muted (with who and why) and the remediation write-upsoff
graph:cypherRun raw CypherSend raw read-only Cypher instead of a questionoff

Run scans. Start work that writes the attack-surface graph.

PermissionUI labelLets the agentDefault
recon:scanStart and stop scansStart a scan (saving the current graph as a version first), and stop the running scanoff
recon:queueQueue scans to run laterQueue a full recon for when the host is free, and cancel one it queued. A queued job outlives the tokenoff
recon:overwriteDiscard the current graph on startStart a scan that discards the current graph instead of saving itoff

Change settings and findings. Writes that are not scans.

PermissionUI labelLets the agentDefault
recon:settingsChange recon tuning settingsChange allowlisted recon tuningoff
triage:writeRecord a verdict on a findingMark a finding confirmed, likely noise or unreviewed, as if you had clicked it. It cannot mute or unmute anythingoff

Open an engagement. Writes that create a project or record what permitted it.

PermissionUI labelLets the agentDefault
project:createOpen a new engagementCreate a project with its targeting mode, settings and authorization record, atomically. Scope is fixed at creation and immutable afterwards. A third-party engagement without a non-zero rate ceiling and an authorization record is refused outrightoff
engagement:authorizeRecord what authorized an engagementAttach a scope document's digest, kind, source and issue date. Append-only: there is no tool here that edits or deletes one, and the record carries the id of the token that wrote it, so a revoked credential is still attributableoff

Only the digest of a scope document is ever stored, never the document. Treat writing one of these as a durable claim you are making: it is what an incident review reads afterwards to establish that a scan was permitted.

Run commands at the target. Set apart in the form, because it is the only permission that reaches a live target outside a scan and the only one that needs three more switches nobody in that form controls.

PermissionUI labelLets the agentDefault
kali:execShell access to the Kali sandboxbash -c with the full toolset, no allowlist and no target check, plus reading and stopping itoff

The defaults now come from the profile you picked, except for two. kali:exec and recon:overwrite are never ticked by a profile, not even by the profiles that recommend them: they appear as "recommended, tick it yourself" with the box clear. Command execution at a live target and irreversible graph destruction must be deliberate choices, not side effects of choosing from a dropdown.

Write permissions do not include reading. A token with only recon:scan can start a scan but cannot call list_projects to find the project id, so in practice every token also needs recon:read.

kali:exec also needs a deployment switch. Ticking it is not enough on its own: MCP_KALI_EXEC_ENABLED=true must be set on the server as well, and it is off by default even when the MCP server is on. Enabling one must never quietly enable the other, because this is the only permission that reaches a live target outside a scan. Inside the product the equivalent action is gated by a person clicking a confirmation. A token has no person, and nothing replaces that check - which is exactly why the three switches, and your judgement about which agent you hand this to, are what the safety rests on.

recon:overwrite is split from recon:scan on purpose. Discarding the current graph is the only thing on this surface you cannot undo. The graph is full of text scraped from live targets, and an agent reading that text can be talked into acting on it. Making destruction a separate, off-by-default permission means the containment is a code check, not a line in a prompt.


Agent Onboarding

Connecting an agent tells it that the tools exist. It does not tell it what RedAmon is, what its recon pipeline produces, or which of thirty-four tools to reach for first. Agent Onboarding writes those instructions for you, tailored to the job you picked and to exactly what the token can do.

Agent Skills teach RedAmon's agent. Agent Onboarding teaches yours. RedAmon's built-in Agent Skills, Chat Skills and Community Agent Skills all point inward: they make RedAmon's agent better. This one points outward.

Reach it from Global Settings → MCP Server: the Agent Onboarding button in the tab header, the Onboard button on any token row, or the reveal panel right after you mint a token.

Choosing an Agent Profile re-ticks the permissions that job needs, while kali:exec and recon:overwrite stay clear and flagged

The Agent Profile

A token is minted for a job. The profile names that job, and it does two things: it ticks the permissions that job needs, and it decides which half of RedAmon the generated instructions concentrate on.

ProfileWhat the agent is forPermissions it ticks
Bug bountyBreadth, dedup, exploitability ranking, program-submittable reportsrecon:read, recon:scan, triage:read, graph:cypher
Penetration testingRules-of-engagement bounded, evidence-backed, client-saferecon:read, recon:scan, triage:read, graph:cypher
Continuous attack surface monitoringNightly rescan, diff, alert on what is newrecon:read, recon:scan, triage:read, recon:queue
Vulnerability managementTurn remediations into tickets someone can workrecon:read, triage:read
Triage assistanceDedupe, prioritize, write verdicts backrecon:read, triage:read, triage:write
Asset inventory / CMDBWhat do we actually exposerecon:read, graph:cypher
Compliance and audit evidenceScan history, versions, proof of coveragerecon:read, triage:read
DevSecOps CI gatingFail a build when a new critical appearsrecon:read, triage:read, recon:queue
Reporting and dashboardsExec summaries fed from the graphrecon:read, triage:read, graph:cypher
M&A and third-party riskMap an unfamiliar estate and judge its posturerecon:read, recon:scan, triage:read, graph:cypher
Threat intel correlationPivot CVE to CWE to CAPECrecon:read, triage:read, graph:cypher
SOC enrichmentIs this alerting host part of our known surfacerecon:read, graph:cypher
Research and trainingSafe targets, reproducible graphs, throwaway projectsrecon:read, recon:scan, recon:settings, triage:read, graph:cypher
CustomA job none of the others describesrecon:read

Seven of the thirteen grant no write of any kind. A profile that does not need to start a scan does not get recon:scan, and triage:write and recon:settings each go to exactly one profile. The two unattended profiles (Continuous attack surface monitoring and DevSecOps CI gating) get recon:queue, because a job with nobody watching that can only start immediately simply fails whenever the project is busy.

The two permissions a profile will never pick for you

kali:exec and recon:overwrite are always left unticked, whatever profile you choose. Penetration testing and Bug bounty recommend the first, and Research and training recommends both, but all three render them as "recommended, tick it yourself" with the box clear and the danger callout showing.

Handing an agent a shell on a target-facing box, and irreversibly destroying a graph, should never arrive as a side effect of choosing an item from a dropdown. That is how over-privileged tokens get minted. kali:exec in particular is a real shell with no allowlist and no per-command target check, so the operator ticking it is the only thing standing between an agent and any host that sandbox can reach.

A profile is not a permission

The profile is a label plus a starting point. Only the ticked permissions are ever enforced: nothing in RedAmon's token resolution, permission checks or rate limiting reads the profile at all. A token's power is exactly its permission list, whatever its profile says.

This matters in the direction people get wrong: switching a token to a narrower-looking profile does not lock it down. If you want it to do less, untick permissions.

Profile and permissions may disagree

Hand-edit freely. When your permissions no longer match the profile's recommendation, the form says so quietly and leaves both alone: a Penetration testing token with kali:exec removed is still a pentest token, and the editorial intent is still worth keeping.

The generated pack always describes only the tools the token can actually call, so divergence can never produce instructions that promise something the agent will be refused.

What is in the pack

The Agent Onboarding modal: profile, permissions, server URL and a preview of the generated SKILL.md

Every export, whatever the profile, opens with the same full explanation and only then narrows:

  1. The operating model. RedAmon has already done the reconnaissance; the agent's job is to mine the graph, and to validate against the live target only where a human authorized it. Stated first, because the most common wrong turn is an agent hunting for an "attack" tool that deliberately does not exist.
  2. What RedAmon is, and what the recon pipeline produces.
  3. The graph's shape - the node taxonomy, the eight finding labels, the CVE-to-CWE-to-CAPEC pivot, and the two states (Muted, stale_since) that change what a finding means.
  4. What the MCP surface can do, by capability area.
  5. Your profile's way of working - the posture, the tool sequence for the routine task, what this job leans on and why, what to ignore, how to report, and the traps specific to that job.
  6. What this token can and cannot do, tool by tool, with the permission each missing one needs.
  7. The ground rules - the untrusted-data rule, the bar for calling something "clean", and the reporting contract.

The tool facts are read from the server's own tool list at export time, so a pack cannot describe a tool differently from how the server serves it, and a tool withdrawn with MCP_DISABLED_TOOLS is absent from the pack automatically.

Two deliveries, one source

Who gets itWhen
The pack (SKILL.md plus reference files)Claude Code, Claude Desktop, and anything else that loads a skill fileYou download it and install it
Inline onboardingEvery client, including Cursor, Windsurf, Cline, Goose, Gemini CLI, Codex CLI and anything built on an agent SDKAutomatically, at connect

Most MCP clients never read a skill file. They get a shorter version of the same guidance the moment they connect, so nothing is missing for them and the download is a document for you to paste or adapt. If you use one of those clients, the export has not failed.

Where to put it

For Claude Code and Claude Desktop, the pack goes in its own skill directory:

~/.claude/skills/redamon-<profile>/
  SKILL.md
  references/
    lifecycle-and-scans.md
    findings-and-fixes.md
    graph-queries.md
    settings.md
    kali-exec.md        (only when the token has kali:exec)

The directory is named after the profile, so two profiles can live side by side in one agent's skill directory.

This is a second install. The MCP server config from Connecting your client lets your agent connect; the pack teaches it what to do. They are separate steps.

Re-export after changes

A downloaded pack is a snapshot, and it stamps the RedAmon version, the profile and the exact permissions it was built for in its header. Export again after you edit the token, change its profile, or upgrade RedAmon. The tab offers to do it for you right after an edit that changes what the token can do.


Raw Cypher (graph:cypher)

With this permission, query_graph accepts a cypher argument in place of a question:

{ "projectId": "<id>", "cypher": "MATCH (d:Domain)-[r]->(s:Subdomain) RETURN s.name LIMIT 50" }

It is exactly as safe as the natural-language path, because both end in the same place. What happens to the query:

  1. Writes are refused. CREATE, MERGE, SET, DELETE, REMOVE, DROP, LOAD CSV and similar are rejected before anything runs. On top of that, the database session itself is opened read-only, so even a cleverly disguised write is refused by Neo4j.
  2. Every node needs a label. A bare MATCH (n) that dumps the whole graph is refused.
  3. Procedures are blocked. CALL is limited to a few read-only schema procedures. Anything like apoc.cypher.run("..."), which would hide a second query inside a string, is refused.
  4. Every node pattern is rewritten to your project. Your query:
    MATCH (s:Subdomain) RETURN s.name
    
    actually runs as:
    MATCH (s:Subdomain&!Muted {user_id: $tenant_user_id, project_id: $tenant_project_id}) RETURN s.name
    
    If any pattern cannot be proven scoped this way, the query is refused rather than run.
  5. Every returned row is checked again on the way out. If any node belongs to another user or project, the whole answer is discarded and a security event is logged.

Things to know when writing Cypher for it:

  • Put the label in the pattern. Write MATCH (p:Package), not MATCH (n) WHERE n:Package. The error message tells you which pattern it could not scope.
  • The write check reads the raw text, so a harmless filter such as WHERE p.title CONTAINS 'set' is refused because the word set appears in it. Use a different way to phrase it, or ask in English.
  • Muted findings are invisible. You cannot match them, and mentioning the Muted label at all is refused.
  • CVE, MITRE and CAPEC nodes are shared reference data. You can reach them by walking from your own findings, but MATCH (c:CVE) RETURN c on its own is refused.
  • Resolved findings are still in the graph. A finding a later scan stopped reporting is kept and stamped with stale_since. Add WHERE x.stale_since IS NULL if you only want current findings.
  • Raw Cypher uses the cheap read rate limit and spends no LLM budget.

Starting and stopping scans

start_recon takes a mode:

ModeWhat happensNeeds
new (default)The current graph is saved as a version in the Scan Timeline, then the scan rebuilds it. This uses up one version slot, so the oldest unpinned version is eventually trimmed.recon:scan
overwriteThe current graph is discarded. It cannot be recovered.recon:scan + recon:overwrite

The answer includes the scanJobId, the new version, and a plain note saying whether the previous graph was saved or discarded.

It is refused when:

  • Something else is working on the project's graph: a recon scan, the in-app agent, a triage run, a version being restored, or a GVM, GitHub Secret Hunt, supply-chain, Secret Multiscanner or AI attack-surface scan. If RedAmon cannot check one of these, it counts as busy. A full scan would wipe the graph underneath them. The UI button is less strict because you can see both; an unattended agent cannot.
  • A scan was already started on that project in the last 5 minutes. This limit is per project, not per token, so holding several tokens does not raise it. A start refused because the project was busy does not use up the 5-minute slot.
  • The project has no target domain or IPs configured.

If RedAmon cannot tell whether the start went through, it says so and tells the agent to call get_recon_status before retrying.

stop_recon stops the full recon scan running on the project, whoever started it. If the orchestrator cannot be reached, it reports that the outcome is unknown rather than claiming the scan stopped.


Changing recon settings

update_recon_settings takes a map of setting name to value. Call get_recon_settings first: it lists what is settable and the current values.

  • 648 of the platform's 714 settings are settable: per-tool enable flags, rate limits, thread and worker counts, timeouts, retries, depth and max caps, severity and status-code lists, container images, wordlists and which pipeline phases run. What each one accepts comes from the settings registry, so this surface and the form enforce the same bounds. The remaining 66 are 19 create-only columns (the engagement's targeting and scope, fixed when the project is opened) and 47 closed outright (credentials, the engagement record, another user's data).
  • Numbers must fall within the same min and max as the project form. An out-of-range value is refused, naming the limit. Nothing is quietly clamped.
  • Any other field is refused by name, with the reason (for example: 'targetDomain' cannot be changed over MCP: engagement scope). One bad key refuses the whole call, so nothing is half-applied.
  • Safe concurrent edits. Pass expectedUpdatedAt (the updatedAt you got from get_recon_settings). If someone saved the project form in the meantime, the call is refused with a conflict instead of silently overwriting their change.
  • Refused while a scan is running. A scan reads its settings once, when it starts, so a change made afterwards would do nothing.

The answer lists what changed and adds two warnings when they apply:

  • queuedJobsNeedingReview: queued scans whose settings no longer match what they were queued with. They are parked until a human reviews them, so the agent should not wait for them.
  • affectedSchedules: enabled scheduled scans that will pick up the new values on their next run.

Rate limits and budgets

All limits are per token unless noted. When one is hit, the error tells the agent how many seconds to wait.

LimitDefaultApplies to.env knob
Reads120 / minuteEverything not named below: list_projects, status, settings, summary, schema, raw Cypher, the findings, remediation, version and reference toolsMCP_RATE_READ_PER_MIN
Questions20 / minutequery_graph with question, run_graph_view, the three analytics views, and get_project_activityMCP_RATE_QUERY_PER_MIN
Writes10 / minutestop_recon, update_recon_settings, cancel_queued_scan, set_finding_verdictMCP_RATE_WRITE_PER_MIN
Scan starts1 per 5 minutes, per projectstart_recon, queue_reconMCP_RATE_START_PER_WINDOW, MCP_RATE_START_WINDOW_MS
Graph comparisons2 per 5 minutes, per projectcompare_scan_versionsMCP_RATE_COMPARE_PER_WINDOW, MCP_RATE_COMPARE_WINDOW_MS
Daily question budget200 / dayquery_graph with questionMCP_LLM_DAILY_BUDGET
Sandbox commands20 / minutekali_execMCP_RATE_EXEC_PER_MIN

kali_output uses the cheap read limit, not the exec one, so watching a slow command costs nothing like starting another. kali_cancel uses the write limit.

get_project_activity sits in the question bucket despite reading nothing billable: answering it costs up to eight requests to the scan orchestrator, which every project on the host shares. Ask it before acting, rather than polling it.

A single question can cost up to 9 calls to your AI provider, because query generation is retried when it fails. The daily budget is per token, so one runaway agent cannot use up your whole account. The account-wide LLM cap still applies on top of it.

Graph queries from MCP also run at most 2 at a time across all tokens (GRAPH_EXEC_MCP_CONCURRENCY), so a looping agent cannot slow down the graph page or running scans. That ceiling covers the findings tools as well as the query ones: they read through the same service your own Priority Board reads, so without it a looping agent would contend directly with your triage screen.

compare_scan_versions is bounded differently, because it is bounded by something else: capturing the live graph takes one of only two snapshot slots shared with version activation, and it refuses rather than queueing when none is free. Comparing two saved versions does not need a slot at all.


Connecting your client

Connecting is two installs, not one. This section is the first: the server config that lets your agent reach RedAmon. The second is the Agent Onboarding pack, which teaches it what to do once connected. Claude Code and Claude Desktop load that pack from a skill directory; Cursor, Windsurf, Cline, Goose, Gemini CLI, Codex CLI and anything built on an agent SDK do not read a skill file at all, and get a short version of the same guidance automatically the moment they connect.

Paste the snippet the mint dialog gives you:

{
  "mcpServers": {
    "redamon": {
      "url": "https://your-redamon-host/api/mcp-server",
      "headers": { "Authorization": "Bearer rdmn_mcp_..." }
    }
  }
}

For Claude Code:

claude mcp add --transport http redamon https://your-host/api/mcp-server \
  --header "Authorization: Bearer rdmn_mcp_..."

Support for a static bearer header on a remote MCP server varies between clients and changes between releases, so test yours end to end before you rely on it in a pipeline.

Checking it by hand

curl -s https://your-host/api/mcp-server \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Authorization: Bearer rdmn_mcp_...' \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

A working token returns the list of tools. Both Accept values are required; leave one out and the server answers 406.

What the response codes mean

CodeMeaningFix
401Token missing, wrong, deleted, revoked or expired. The reason is never told to the caller.Check the token list in the tab: revoked and expired tokens are flagged there, and a deleted one is simply absent. An expired token can be extended with Edit; a revoked or deleted one needs a new token.
404The server is switched off.Set MCP_SERVER_ENABLED=true in .env, then docker compose up -d webapp. A plain docker compose restart keeps the old value.
403A browser Origin that is not your RedAmon host, or the deploy's edge gate.Call from a non-browser client; on a deploy see the gates below.
405A GET or DELETE. The server only takes POST.Use POST.
406The Accept header does not include both application/json and text/event-stream.Send both.
413Request body over 64 KB.Send smaller requests.
415Content-Type is not application/json.Set it.
400Invalid JSON, or several JSON-RPC requests batched into one.Send one request per call.

Inside a successful call, a tool can still return an error the agent can read:

  • This token is missing the required scope: recon:scan: mint a token with that permission.
  • Project not found: the id does not exist or belongs to someone else. Both give the same answer on purpose, so nobody can discover other users' projects.
  • Rate limit reached for this token. Try again in 42s.
  • Scan status is unknown: the orchestrator is unreachable.: this is never reported as "not running".

If you are running on a deployed server

On a hardened deploy there are three gates in front of the endpoint, and all three must admit your agent. Two of them are invisible from inside the app, which is why "I enabled it and it still does not work" is almost always one of these:

GateSet it withIf it refuses you see
Cloud Security Groupyour provider's consolea timeout
Host firewall (ufw)MCP_CLIENT_CIDRSa timeout or refused connection
nginxMCP_CLIENT_CIDRS, MCP_EDGE_ALLOW_BEARER403

The firewall filters by port, not URL, so it cannot tell an MCP call from a visit to the UI. When OPERATOR_ALLOW_CIDRS is set, an agent connecting from anywhere else is dropped before nginx ever runs. Set MCP_CLIENT_CIDRS to your agent's egress range: those addresses reach /api/mcp-server and nothing else, so you are not widening access to the UI.

If the deploy uses GATE_MODE=basic_auth, the browser password and the token would both need the same Authorization header, which is impossible. The endpoint answers 403 until you set MCP_EDGE_ALLOW_BEARER=true, which turns the basic-auth prompt off for that one URL only.

MCP is also refused entirely over plain HTTP. Your token travels in a header on every call and outlives your session, so one capture is a lasting credential. With a self-signed certificate most MCP clients will refuse to connect; use a real certificate or a domain.

On a deploy, 10 failed token attempts within 2 minutes ban the source IP for 30 minutes (fail2ban jail redamon-mcp-auth). A normal agent that is only being rate limited never trips it.

Run ./deploy.sh verify after enabling. It probes the endpoint and tells you which gate is refusing: 401 means enabled and working, 404 means the switch did not reach the webapp, 403 means the edge gate is eating the token.


Managing tokens

The list shows every token with its name, first 8 characters, permissions, creation date, expiry and last use. "Last used" is updated at most once a minute per token.

Each row's actions are behind the menu at its right: Onboard, Edit and Delete. They live in a menu rather than as three spelled-out buttons because the buttons were the widest thing in the row and pushed the permission tags, which are what you actually read the table for, into wrapping.

Edit token panel: a picked expiry date and an added permission, which asks for the password

Changing the profile on an existing token asks before it resets the permissions, and tags the ones that differ

  • Delete removes the token from the database. The agent's next call fails, not its next reconnect, and the row disappears rather than staying flagged. It asks first, and it cannot be undone.
    • What survives is the audit record. The audit log keeps that the token existed, its first 8 characters, its permissions and who removed it when, so an incident review can still match it against the MCP call log. Deleting the row does not erase its history.
    • Available on every row, including expired and revoked ones: a row nobody needs any more is exactly the one worth clearing out.
  • Edit changes the name, the Agent Profile, the permissions and the expiry. Changes apply to the agent's very next call.
    • Expiry can be kept, ended right now (Expire now), set to 30, 60 or 90 days or 1 year from today, set to a date you pick (the token works until the end of that day, UTC), or removed.
    • Expire now vs Delete: both stop the agent at once, but an expired token stays listed and can be extended again later; a deleted one is gone.
    • Taking power away needs no password: removing a permission, an earlier expiry, Expire now, a new name.
    • Giving power needs your password again, exactly like creating a token: adding a permission, a later expiry, removing the expiry, or bringing an expired token back. A stolen browser session therefore cannot upgrade an existing token any more than it can create a new one. Wrong passwords count toward the same lockout as token creation.
    • Changing the profile grants nothing, so it never asks for your password. It re-suggests that profile's permissions, and if you accept them, those are judged on their own merits: one that adds a permission asks for the password like any other widening, one that only removes permissions does not.
    • Hand-editing the permissions away from the profile is fine. The form points it out and changes nothing on its own. The profile is a label and a starting point, never a permission.
    • A revoked token can only be renamed or have its profile changed. To restore access, create a new token.
    • After an edit that changes what the token can do, the tab offers to re-export its onboarding pack, because the one you already gave that agent now describes a token that no longer exists.
    • Edit never changes the token itself. The agent keeps using the same rdmn_mcp_... value. If you think the token has leaked, don't just remove permissions: delete it and create a new one.
  • Expired and revoked tokens stay listed, flagged, until you delete them. "Why did my agent stop working" is the common question, and hiding the answer makes it harder. Left alone, they are removed automatically 90 days after they expired or were revoked (MCP_TOKEN_RETENTION_DAYS).
  • Changing your password revokes every token, including when an administrator resets it for you. A reset that left live credentials behind would not really lock the account.

An administrator can list your tokens and take power away from them during an incident: rename, remove permissions, shorten or end the expiry, and delete. They cannot mint a token on your account or give an existing one more power, not even while viewing your settings; the tab shows a notice and disables those actions. A credential created or upgraded that way would outlive their session, need no further authentication, and be indistinguishable from your own calls.


What a token can never do

This is the part worth reading twice.

It cannot change what RedAmon is pointed at. Not the target domain, not the IP list, not the subdomain seed list, not the domain batch, not the scope guardrail.

The reason is blunt: if a token could set targetDomain and switch off the guardrail, then "let my agent rescan my own projects" would quietly become "let my agent scan anyone, with the safety off". That is the boundary the whole product's legal posture rests on. Every targeting column is fixed at creation and refused by name afterwards, never silently dropped, so your agent always knows a setting did not apply. A test asserts every one of the 700-plus project columns is explicitly classified, so a field added next month has a stated disposition rather than an accidental one.

It cannot touch the engagement RECORD. Not the client name, the contacts, the emergency number, the dates, the compliance frameworks or the uploaded document. Those are the contract: a person writes them, a model reads them, and nothing in the pipeline enforces them, so they carry a third party's personal data and no benefit from being machine-writable.

It CAN change the engagement's limits, and that is deliberate. The rate ceiling, the never-touch hosts, the scanning window, the forbidden tools and categories, the severity cap: ordinary settings, writable in either direction. What keeps them honest is not a write-time direction rule — one of those shipped here and five of the fields it covered accepted a widening while reporting a tightening — but that every limit is enforced at scan start whatever the setting says. A ceiling of 3 rewrites all 17 rate fields; an excluded host is dropped in three places in the pipeline. Call preflight_scope_check and report the RESOLVED values: that is the check that actually holds.

It cannot switch the limits off while leaving them configured. There is no master flag to write. The limits apply when there IS a limit to apply — a non-zero ceiling, a non-empty exclusion list, or a window — and that is derived rather than stored, so removing a limit means removing it, visibly, rather than flipping one boolean and leaving every field displaying its old value.

It cannot spawn an arbitrary container. Every *DockerImage field is a closed list of the shipped images, and a value outside it is refused at the write rather than accepted and silently replaced at scan start.

It also cannot reach the other scanners' targets, the wordlists, templates, custom headers or out-of-band callbacks beyond their validators, or one stored credential.

kali_exec is the exception, and you should read it as one. Everything above describes what a token cannot do by enforcement. kali_exec is not enforced: it is a shell, it has no target check, and an agent holding kali:exec can reach any host the sandbox can - including one this project is not for.

That is the same position RedAmon's in-app agent is in, and it is why the permission is off by default at three levels and why the per-project switch cannot be turned on over MCP. The protection is that granting it is three deliberate human decisions, not that the software will catch a bad command afterwards. Do not grant it to an agent you would not trust with a terminal.

It cannot reach another user's data. A token is one user. A project id that is not yours returns "not found", the same answer as a project id that does not exist, so the tool cannot be used to discover what exists. Every graph query is rewritten to your project before it runs, and every row that comes back is re-checked against your tenant on the way out; anything that fails drops the entire response and raises an audit record.

It cannot hide a finding, or reveal one somebody hid. A token can record a verdict, which ranks a finding. It can never mute or unmute one, which hides or reveals it. Muting is the single action that makes a finding invisible to every other tool here, and unmuting reverses a decision a person made. That bound is what keeps a manipulated agent to mislabelling something you can overrule, rather than making it disappear.

It cannot write anything else to the graph. Every read runs in a read-only database session, and a verdict is the one write on this surface.

It cannot quietly spend your money. Natural-language queries use your configured LLM provider, so each token gets its own daily budget and its own rate limits on top of the account-wide cap. Raw Cypher and every other tool spend nothing.


What it deliberately refuses to do

  • Start a scan while you are working. If you have the in-app agent open on that project, or a triage run going, start_recon refuses. A full scan rebuilds the graph from scratch and would pull it out from under you.
  • Change settings mid-scan. Recon reads its settings once, when the scan container starts. A change made afterwards would do nothing, so the tool says so instead of reporting a success that is not one.
  • Answer "nothing found" when it means "could not ask". If the orchestrator is unreachable, status comes back as unknown, never not running. If the graph cannot be read, that is an error, never an empty result.
  • Compare a graph that moved while it was reading it. Swapping a saved version in is not atomic, so a comparison that straddled one would report your whole attack surface as deleted. compare_scan_versions checks before and after, and refuses rather than returning a difference that never existed.
  • Record a verdict a triage run is about to overwrite. A run reads at its first step and publishes minutes later, so a verdict written in between would be silently re-filed. set_finding_verdict refuses while a run is in progress, and refuses too if it cannot tell whether one is.
  • Leak internals in errors. Errors are short, fixed sentences. Stack traces, file paths, host details and upstream messages go to the server log only, never to the agent.

Audit

Every call is recorded: which tool, which project, which token (by id and prefix), and what happened. Failures too: a revoked token being presented, a permission denied, a project id that was not yours. That is the only way a brute-force attempt or a leaked-then-revoked token becomes visible.

ActionRecorded when
mcp.<tool>Every tool call, with outcome ok, scope_denied, access_denied, rate_limited, busy, and so on
mcp.start_recon, mcp.start_recon.refusedA scan start, with mode, scanJobId and version
mcp.update_recon_settingsA settings change, with before and after values
mcp.kali_exec, mcp.kali_exec.refusedEvery sandbox command, verbatim. Refusals too: a command aimed outside your scope is the clearest sign an agent has been talked into something, and this is the only record of what it tried
mcp.set_finding_verdictA verdict, with the finding, the status and the token. A verdict is durable and stops later AI review of that finding, so it is recorded on both the webapp and the agent side
mcp.queue_recon, mcp.cancel_queued_scanWork queued or un-queued, with the job id
mcp.kali_cancelA command stopped, with its job id
mcp.auth.deniedA bad token (sampled to one row per token prefix per minute, so a flood cannot fill the database)
mcp-token.create, mcp-token.rename, mcp-token.revokeChanges made in the tab
mcp-token.updatePermissions or expiry edited, with before and after values and whether the edit gave the token more power (widened)
mcp-token.revoke-allA password change revoked your tokens

A scan started over MCP shows up in the Scan Timeline exactly like a manual one, attributed to you.

There is no audit-log viewer in the product yet; reading it is a SQL query against the audit_log table, for example:

SELECT created_at, action, target_id, after
FROM audit_log
WHERE source = 'mcp'
ORDER BY created_at DESC
LIMIT 50;

Regenerating the API reference

The MCP API Reference page is not written by hand. A script asks the MCP server for its tool list, exactly as a connected agent would, and turns the answer into Markdown. There is no AI involved: the same code always produces the same page.

What comes from the code, and where. Everything on that page is read from webapp/src/lib/mcp/server.ts:

On the pageComes from
Tool names, titles and descriptionsThe registerTool(...) calls (the same descriptions the agent reads)
Arguments, types, required flags, length and pattern limitsEach tool's inputSchema
PermissionsEach tool's _meta: scopesMeta({...}) declaration
Read-only / destructive / idempotentEach tool's annotations
Permission labels and explanationswebapp/src/lib/mcp/scopeCopy.ts, shared with the token screen in the UI

This page (MCP Server) stays hand-written, including the tool table near the top.

When to run it. After any change to a tool: a new tool, a renamed argument, an edited description, a different permission. Nothing else needs it.

How to run it. From the main RedAmon repo, with the wiki checked out at redamon.wiki/ in the repo root:

cd webapp
npm run docs:mcp          # writes ../redamon.wiki/MCP-API-Reference.md

It needs webapp/node_modules (npm ci once) but no database and no running stack; it takes about a second. If your wiki checkout lives somewhere else, point at it:

MCP_DOCS_WIKI_DIR=/path/to/redamon.wiki npm run docs:mcp

Then publish it like any other wiki edit:

cd ../redamon.wiki
git add MCP-API-Reference.md
git commit -m "docs: regenerate MCP API reference"
git push

You cannot forget it. The same file, webapp/src/lib/mcp/apiReference.test.ts, runs with the normal webapp tests (npm run test, and ./redamon.sh test). It renders the page in memory and compares it with the wiki file. If they differ, the test fails with MCP-API-Reference.md is stale: run npm run docs:mcp in webapp/. On a checkout without the wiki there is nothing to compare, so that one check is reported as skipped, not passed.

The test also keeps the permissions on the page honest. It calls every tool through a real MCP client with each declared permission taken away and checks the call is refused naming that permission, and that the declared permissions are enough. A permission written in the page but not enforced in the code, or the other way round, fails the test before it can be documented.

The copy on redamon.org/docs is refreshed from the wiki separately, like every other page.