/mcp is now /agents: a table of agents with access, scope, last call and
expiry, and a row menu for History, Edit access and Cut access. History,
the tool catalog, server settings and the connect flow open in sheets.
Connecting names the client and hands over its own setup snippet with the
key filled in (mcp/clients.py), then waits for the key's first call.
PATCH /mcp/tokens/{id} edits a key's project and access, and a key
reports the projects and targets it reaches.
Every tool call is recorded, refusals and invalid arguments included,
with the key id, a readable line of arguments, the result's summary and
its pivot, so History opens the rows the agent was given.
reNgine MCP server
Lets a language-model agent interrogate an attack surface reNgine has already mapped — and, if you allow it, start scans.
Everything MCP lives in this directory. Turn the server on, issue a token and paste it into your agent; nothing else is configured, and no file outside this package needs editing to add a tool.
- Start / stop, tokens and activity: the MCP page in reNgine.
- Adding your own tool: ADDING_A_TOOL.md.
What makes this server different
Most recon MCP servers expose one verb: go run a scan. This one is built the other way round, because reNgine's value is the correlated result set, not the scanner. Four properties are carried into every answer:
The count is a promise. Every result carries open_in_rengine. The number a
tool reports equals the number of rows that link opens, because both go through
the same filter and compiler the UI uses. An agent can never quote a figure a
human cannot verify in one click.
Not scanned is not zero. resolve_target reports covered per dimension.
A scan that enumerated hostnames but never probed them has zero live hosts and
never looked — those are different facts, and the tools refuse to conflate them.
New needs a baseline. is:new is has_baseline AND NOT seen_earlier
everywhere, so a target's first scan never reports its whole surface as new.
Rows are untrusted input. Response bodies, page titles, service banners and
certificate subjects are written by the scanned party. Results that contain them
are flagged untrusted_content, and the server's instructions tell the model to
report them, never to follow them.
Connecting
Issue a token on the MCP page, choose Copy client config, and paste it into your agent. That is the whole setup.
{
"mcpServers": {
"rengine": {
"type": "http",
"url": "http://localhost:5173/api/v1/mcp",
"headers": { "Authorization": "Bearer rngmcp_…" }
}
}
}
For an agent that prefers to launch the process itself:
docker compose exec -T api /app/.venv/bin/python -m mcp.stdio
with RENGINE_MCP_TOKEN in its environment. Permissions are identical on both
transports — they are a property of the token, not the pipe.
Capabilities
A token carries a subset of four capabilities, and the instance sets a ceiling
no token may exceed. tools/list returns only the tools a token may call, so an
agent never sees a tool it cannot use.
| Capability | Grants | Reaches the target |
|---|---|---|
read |
Query assets, services, endpoints, findings, coverage | No |
plan |
Resolve a scan plan without running it | No |
write |
Add, label and delete targets; record triage decisions | No |
launch |
Start and stop scans, and focused rescans | Yes |
read is always granted. launch is off by default at the instance ceiling.
The default tools
Twenty-one tools, roughly 4,400 tokens of definitions for a full-capability token. Arguments in bold are required.
Orient — turn a name into something you can query
| Tool | Needs | Arguments |
|---|---|---|
resolve_target |
read | target |
surface_brief |
read | target, dimension, include_empty |
describe_query_language |
read | dimension, fields_only |
list_targets |
read | contains, limit |
list_projects |
read | — |
list_engines |
read | contains, project_id, limit |
list_contexts |
read | project_id, limit |
scan_status |
read | scan, target, limit |
resolve_target is the keystone. Every result endpoint in reNgine is
scan-scoped, and picking the newest scan is wrong. This resolves, per dimension,
the most recent scan that actually covered it — plus the count, when it was
observed, and the change against the previous covering run. Every other tool
takes its scan from here.
surface_brief is the fastest way to learn what is interesting. It returns
reNgine's curated query library with a real count against each entry for this
scan, ranked so the discriminating queries come first. One call, and the agent
knows where to look instead of guessing:
class:database 3 Data stores reachable from the internet
is:sensitive and not is:cdn 33 Administrative ports on an origin address
class:remote 8 Interactive administration reachable
service:[telnet,ftp,rlogin,rexec] 6 Protocols that carry credentials in the clear
describe_query_language returns the exact grammar for one dimension. It is
a tool rather than part of a tool description because all five grammars together
are about 16,600 tokens — far too much to carry on every turn, and exact when
fetched.
Interrogate — ask the question
| Tool | Needs | Arguments |
|---|---|---|
query_assets |
read | target, dimension, query, limit, offset |
group_assets |
read | target, dimension, group_by, query |
what_changed |
read | window, project_id |
query_assets is one tool over all five dimensions. Rows are trimmed to the
columns that carry meaning — a raw row costs about 205 tokens, a trimmed one
about 25 — and the exact total always comes back with the link that proves it.
group_assets counts by technology, status, ASN, country, severity, service
class and so on. Each group carries the query that isolates it, so drilling in
lands on exactly that count.
what_changed is the cross-target digest: what is new per target over a
window, which dimensions a run covered for the first time, and which targets have
never been scanned or have gone stale.
Explain — turn a row into an answer
| Tool | Needs | Arguments |
|---|---|---|
explain_finding |
read | target, finding |
scan_coverage |
read | target, dimension |
explain_finding takes a template id, check name, CVE or fingerprint and
returns what the check tests for, why it matters, how to fix it, its CVE / CWE /
CVSS / EPSS / KEV signals, every place it fires, and the current review decision.
scan_coverage is the scanner's own account: checks selected against checks
loaded, hosts scanned, requests sent, hosts given up on. A null count means the
scanner did not report that number — never that it was zero. This is what keeps
"no findings" honest.
Act — capability-gated
| Tool | Needs | Arguments |
|---|---|---|
plan_scan |
plan | target, engine_id, stages, intensity |
start_scan |
launch | target, engine_id, stages, intensity, context_id |
focused_rescan |
launch | target, dimension, assets, stages |
record_triage |
write | target, fingerprint, state, note |
add_target |
write | targets, project_id, tags, organizations |
update_target |
write | target, display_name, tags, organizations |
delete_target |
write | target, confirm |
cancel_scan |
launch | scan, target |
plan_scan resolves which stages would run, which are skipped and why, the
footprint and the estimated duration, without contacting the target at all.
list_engines and list_contexts are what make engine_id and
context_id usable — nothing else in the server can produce one, so without them
an agent can only ever assemble stages itself and never run the configuration a
person saved. Neither returns a credential: a context reports what it holds
(Basic (admin), three excluded ranges, a proxy) and never the secret.
scan_status is the other half of start_scan, which returns a scan id no
other tool used to accept. It reports the stages done, running and failed, and
what the run has found so far — poll it rather than waiting. Its counts are the
run's own rollup, which is not the same figure resolve_target gives for a
cancelled run; the caveat says so.
cancel_scan is the brake. Launch without one is an asymmetry, not a missing
feature: an agent that starts a scan against the wrong host has to be able to
stop it, so this needs no confirmation and takes effect immediately. What the run
already found is kept; the dimensions its remaining stages would have covered
were never scanned, which is not the same as finding nothing.
focused_rescan is the loop that makes an agent useful: query, pick the
interesting rows, re-probe just those as their own run, read the result. The run
is recorded separately and does not disturb the parent scan's totals.
record_triage makes an agent's judgement durable. The decision is keyed to
the finding's fingerprint, so a false positive stays suppressed on every later
scan of that target.
add_target records a target and queues its WHOIS, DNS and routing
enrichment. It sends no traffic — a target reNgine knows about is not a target
reNgine has scanned, and every dimension reads as never scanned until a scan
runs. A value already in the project is reused, so an agent that re-adds its
working set does not duplicate it.
delete_target is the one tool that destroys data, and it is the only one
whose destructiveHint is true. Called without confirm it refuses, and
answers with what would be lost — the number of scans and what they hold — so
the agent has something to put in front of a person before asking again.
Layout
mcp/
capabilities.py what a token may do — one definition, mirrored by the frontend
auth.py minting, hashing and header parsing; reNgine keeps only the hash
models.py the mcp_tokens table and the API's read/write shapes
settings.py server settings, stored on instance_settings
service.py settings, tokens, status, authentication — what the API calls
protocol.py JSON-RPC framing and the MCP method names. No transport, no DB
server.py dispatch: a parsed request plus a context becomes a response
transport.py one entry point: authenticate, rate-limit, dispatch
stdio.py the stdio transport
registry.py every discovered tool, validated and described
dimensions.py the five result dimensions, adapted for tools
links.py deep links back into the UI
limits.py per-token call ceiling (fail-open)
telemetry.py live sessions and the recent-call trail (Redis, fail-open)
tools/ one file per tool, discovered automatically
The wire protocol is implemented directly rather than through an SDK: a
tools-only MCP server is a small JSON-RPC surface, and it keeps this package
dependency-free. protocol.py is the only module that knows the wire format, so
swapping in an SDK later touches one file.
Note that this package is named mcp, which shadows the mcp package on PyPI.
That is deliberate — reNgine does not use it — but do not add it as a dependency
without renaming this package first.