MCP Server
Ostorlab exposes an MCP server, letting AI assistants and agent frameworks work with your scans, vulnerabilities, tickets and assets directly — no custom GraphQL client required.
Where the GraphQL API gives you the full platform surface to build against, the MCP server gives an AI client a curated set of typed tools it can discover and call on its own. Ask your assistant "what did the last scan of my app find?" and it will pick the right tools, chain them, and answer.
Endpoint
https://api.ostorlab.co/apis/mcp/{api_key}/
The transport is streamable HTTP. The API key is part of the URL path.
Warning
The trailing slash is required. Without it the server answers with a 307 redirect. Clients that follow redirects will still work but pay an extra round trip on every request; clients that do not follow redirects on POST will fail.
Warning
The key in the URL is a credential. URLs are commonly recorded in proxy logs, browser history and shell history, and MCP client configuration files are usually stored in plain text. Treat the endpoint URL exactly as you would the key itself. To retire a key, revoke it — setting an expiry date does not stop the key from working on this path.
Getting an API key
Create one from the web application under Integrations/API → API Keys
(https://report.ostorlab.co/integrations/api). The key's role determines what the MCP tools
can do — see Permissions.
Connecting a client
Most clients take a server name, a transport and a URL. The transport is streamable HTTP, so
where a client asks for a type, use http.
Claude Code
claude mcp add --transport http ostorlab https://api.ostorlab.co/apis/mcp/YOUR_API_KEY/
Claude Desktop, Cursor, VS Code, Windsurf, Zed
These read a JSON configuration file. The location differs per client and per platform — check your client's documentation for where it lives — but the entry has the same shape:
{
"mcpServers": {
"ostorlab": {
"type": "http",
"url": "https://api.ostorlab.co/apis/mcp/YOUR_API_KEY/"
}
}
}
Some clients name the section servers rather than mcpServers, and some use serverUrl in
place of url. If yours rejects the block above, check its documentation for the key names — the
transport and the URL are the parts that matter.
Confirming it works
Ask the client to run a read tool, for example listing your scans. A bad key returns
Invalid API Key. Asking it to list its tools is not a useful check: the tool list is returned
for any key, valid or not.
Permissions
Every tool checks the API key's permissions before doing anything. A key carries a role. A role grants a set of actions. Each tool requires one action.
| Tool type | Required action |
|---|---|
| Reading scans, vulnerabilities, tickets, comments and checklists | read |
| Creating or modifying tickets, comments and checklists | write |
| Triaging vulnerabilities | write |
| Starting, re-running and stopping scans | write |
| Listing attack-surface assets and tags | attack_surface_read |
| Accepting or rejecting discovered assets, and adding, changing or removing tags | attack_surface_audit |
| Reading and triaging the discovery queue, and reading the attack-surface graph | attack_surface_audit, except the graph which needs attack_surface_read |
| Managing API keys, users and single sign-on, reading the audit log, reading and changing organisation settings, managing ticket agents, scanners and scanner groups, creating automation rules, and configuring, reading or deleting most integrations | admin |
A key without the required action gets a clear error rather than a partial result.
Roles map to actions like this:
| Role | read |
write |
admin |
attack_surface_read |
attack_surface_audit |
|---|---|---|---|---|---|
reader |
yes | no | no | yes | no |
user |
yes | yes | no | yes | yes |
admin |
yes | yes | yes | yes | yes |
attack_surface_auditor |
no | no | no | yes | yes |
Two things follow from that table. attack_surface_read is held by every role, so any key can
call list_assets and list_tags. And an attack_surface_auditor key has neither read nor
write, so it is limited to the attack-surface tools: it can read assets and tags, and it can
accept, reject and edit them, but it cannot see a scan, a vulnerability or a ticket.
If your organisation has object-level access enabled, tools respect it, following the same rules as the GraphQL API-key path:
- Scans and tickets — a non-admin key only sees the individual scans and tickets it has been granted.
- Assets — the grant is on the owner, not the asset. A key sees every asset belonging to
the owners it has been granted. Use
list_ownersto see which owners a key can reach, and therefore which assets it can see.
Available tools
The tables below group the tools by what they work on. Each table gives the action the key must hold, so you can tell at a glance which tools a key of a given role can reach.
The server always describes itself. Every tool carries its own description and its own argument list, and a client reads both when it connects. If a tool is not listed here, ask your client to show its tool list — that is the authoritative answer for the server you are talking to.
Scans
| Tool | Action | Description |
|---|---|---|
list_scans |
read |
List scans, filtered by status, asset_id, scan_profile, risk_rating, created_after, archived. |
get_scan |
read |
One scan's details. |
get_scan_status |
read |
A scan's progress, its recent status history, and any errors it reported. |
list_scan_profiles |
read |
The scan profiles the organisation can run, with the exact profile names create_scan expects. |
search_store_applications |
read |
Search the public mobile stores for an application, to get the name and package create_scan expects. |
create_scan |
write |
Start a scan against a web, network or mobile-store target. |
create_source_code_scan |
write |
Start a scan of a git repository. |
create_source_code_archive_scan |
write |
Start a scan of a source archive you have uploaded. |
create_mobile_file_scan |
write |
Start a scan of a mobile binary you have uploaded. |
create_multi_asset_scan |
write |
Start one scan covering several targets at once. |
create_mobile_testflight_scan |
write |
Start a scan of an iOS build distributed through TestFlight. |
create_autodiscovery_scan |
write |
Start a scan across the organisation's discovered attack surface. |
create_rescan |
write |
Re-run an existing scan with the same asset, profile, credentials and settings. |
stop_scan |
write |
Stop a running scan. |
generate_scan_report |
write |
Produce a PDF report for a scan and return a link to download it. |
get_scan and get_scan_status do not return archived scans, even though list_scans can list
them. Calling either on an archived scan returns Scan not found.
create_scan takes an asset_type of web, network or mobile_store, and the target fields
that go with it: urls for web, networks for network, and mobile_asset_type plus
application_name and package_name for a mobile store target. It also takes a
scan_profile_name, which must be one of the names list_scan_profiles returns. A name the
organisation cannot run is rejected up front, and the error names the profiles that are valid, so
call list_scan_profiles first.
create_rescan is the shorter path when the target has been scanned before. It takes only the id
of an existing scan and copies everything from it, so there is no profile or asset to get wrong.
The new scan is titled Rescan of <original title>, and it is always visible even when the
original was hidden.
Other tools start a scan against a target create_scan does not cover.
create_source_code_scan takes a git repository. create_mobile_testflight_scan takes an iOS
build distributed through TestFlight. create_autodiscovery_scan takes no target at all — it
scans the organisation's whole accepted discovery set, so what it covers is whatever
list_autodiscovery_assets has had accepted into the inventory. create_multi_asset_scan takes
several targets together — web, network, repository, mobile-store and uploaded files — and runs
them as one scan.
Two of them take a file rather than a target you can name: create_mobile_file_scan scans a
mobile binary, and create_source_code_archive_scan scans a source archive. Both take an upload
handle, described next.
Uploading a file to scan
A tool argument is JSON, so it cannot carry the bytes of a file. Upload the file first and pass the handle you get back to the scan tool.
curl -X POST https://api.ostorlab.co/apis/upload/ \
-H "X-API-Key: <your-api-key>" \
-F "file=@app.apk"
The response carries an upload_id, along with the filename, size and SHA-256 the server
recorded, so you can check the file arrived intact:
{
"upload_id": "de6024a2-8af5-4e0c-87d2-fdc2c306225f",
"filename": "app.apk",
"size": 4171034,
"sha256": "4ad2930d93c3af606582b26c55ac285485e9623bb508dbf05e5d391b8e4b0969",
"expires_at": "2026-09-10T09:44:04Z"
}
Pass that upload_id as application_upload_id to create_mobile_file_scan, as
archive_upload_id to create_source_code_archive_scan, or in one of the upload lists
create_multi_asset_scan takes.
Three things to know about a handle:
- It belongs to the organisation that uploaded it. A handle from another organisation is refused
the same way an unknown one is, with
Upload not found. - It expires. After that it is refused with
Upload has expired. - A scan spends it. Once the scan is created the handle stops resolving, so upload again to run a second scan. A call that fails leaves the handle usable, so a retry needs no re-upload.
Warning
Every create_* tool here starts a real scan and consumes scan credits. Have your client
show you the target and the profile before it calls one.
Warning
generate_scan_report blocks until the PDF is finished, which can take a few minutes. A slow
answer is not a failure. Do not time out or retry before it comes back. The response carries
a url, not the file itself. Fetch that URL over HTTPS with the same API key in an
X-API-Key header.
AI pentests
An AI pentest — also called an agentic deep scan — is a separate family from a regular scan. Its ids are not the same ids as a scan's, and its results are called risks rather than vulnerabilities.
| Tool | Action | Description |
|---|---|---|
get_agentic_deep_scan |
read |
One AI pentest: its state, target, timing and summary. |
get_ai_pentest_usage |
read |
The AI token usage one AI pentest consumed. |
list_risks |
read |
The risks an AI pentest found, filtered by pentest and by severity. |
list_ai_provider_api_keys |
read |
The AI provider API keys the organisation has stored. |
Note
Risks are not returned by list_vulnerabilities or search_vulnerabilities, and an AI
pentest is not returned by get_scan. Passing an AI pentest id to get_scan answers
Scan not found. — which means "wrong tool", not "does not exist". A client that only knows
the vulnerability tools will report an AI pentest as having found nothing.
Vulnerabilities
| Tool | Action | Description |
|---|---|---|
list_vulnerabilities |
read |
Findings in one scan, filtered by scan_id, risk_rating, kb_detail_id, or the ticket they belong to. |
search_vulnerabilities |
read |
Findings across every scan, filtered by asset, date range, severity, knowledge-base detail and free text. |
get_vulnerability |
read |
One finding: technical detail, exploitation detail, CVSS vectors and location metadata. |
update_vulnerability |
write |
Triage: set a custom risk rating or CVSS vector, or mark a finding false positive or an accepted exception. |
Use search_vulnerabilities when the question spans more than one scan — "every critical on this
application", "what did we find last month". Use list_vulnerabilities only when you already
have one scan id. Do not loop list_vulnerabilities over a list of scans to answer a question
search_vulnerabilities answers in a single call.
Two things to know about update_vulnerability:
- Marking a finding false positive or exception acts on the tickets linked to that finding. A finding with no ticket is unchanged, and the call still reports success.
- Its
all_vulnerabilitiesparameter applies the change to every finding in the same scan sharing the same knowledge-base detail, and recomputes the scan's overall risk rating. The blast radius is much larger than a single finding.
Every finding carries a dna field — a fingerprint derived from the finding's title, technical
detail and location. The same issue found again in a later scan keeps the same dna as long as
those three are unchanged, which makes it useful for deduplicating when you import findings
elsewhere. It is not guaranteed stable when the technical detail varies between runs.
Scan artefacts
A scan collects more than findings. These tools read what one scan captured while it ran.
| Tool | Action | Description |
|---|---|---|
list_api_endpoints |
read |
The API endpoints a scan discovered. |
list_http_traffic |
read |
The HTTP exchanges a scan captured. |
list_http_folders |
read |
The HTTP folders a scan observed. |
list_pcap_files |
read |
The packet captures a scan produced. |
list_ide_files |
read |
The files found inside a scan's binary. |
list_ide_functions |
read |
The functions decompiled from a scan's binary. |
list_ide_logs |
read |
The device-side runtime logs a scan captured while it drove the app. |
list_call_ui_nodes |
read |
The screens a mobile scan reached while driving the app. |
list_stack_traces |
read |
The call stacks a scan recorded while it drove the app. |
What a scan collected depends on what it ran. A web scan has HTTP traffic and no decompiled binary; a mobile scan can have both. An empty result means that scan collected nothing of that kind, not that the scan failed.
Tickets
| Tool | Action | Description |
|---|---|---|
list_tickets |
read |
Tickets, filtered by status, priority, assigned_email, agent, tag, title, created_since, modified_since. Sortable, with a limit. |
get_ticket |
read |
One ticket, by id or by key (for example os-1234). |
create_ticket |
write |
Create a ticket, optionally with tags, an assignee, a ticket agent, a due date. |
update_ticket |
write |
Change any of the same fields. |
delete_ticket |
write |
Delete a ticket. Not reversible. |
Comments and checklists
| Tool | Action | Description |
|---|---|---|
list_comments, create_comment, update_comment, delete_comment |
read to list, write for the rest |
Comment thread on a ticket. |
list_checklists, create_checklist, update_checklist, delete_checklist |
read to list, write for the rest |
Remediation checklists attached to a ticket. |
create_checklist_item, update_checklist_item, delete_checklist_item |
write |
Individual checklist items. |
Note
Adding a comment to a ticket that has a ticket agent assigned will start an agent run.
Comments created through MCP have no author attached, because the server authenticates an API key rather than a user. They render without an author in the web application.
Ticket agents
A ticket agent is an AI agent the platform can run on a ticket to do the remediation work. You route a ticket to an agent by assigning it.
| Tool | Action | Description |
|---|---|---|
list_ticket_agents |
read |
The agents tickets can be routed to, with an enabled flag and a summary of what each one runs. |
assign_ticket_agent |
write |
Assign an agent to a ticket, which starts a run. |
create_ticket_agent |
admin |
Add a new agent, optionally with the run definition it should use. |
update_ticket_agent |
admin |
Change an agent's name, description or run definition. |
delete_ticket_agent |
admin |
Delete an agent. |
Warning
Assigning an agent starts a run, and a run cannot be undone. Once a ticket has an agent, every
later comment on that ticket starts another run. Read the agent_run field in the response to
see whether this run actually started or was skipped — do not assume it began.
Deleting an agent clears the assignment on every ticket that held it. The tickets survive, but
their agent is set to none, and the only way back is to create a new agent and assign it ticket by
ticket. The response carries an unassigned_tickets count, so check that number before deciding an
agent is unused. The agent's run definition is also destroyed, and it cannot be read back in full
before deletion — list_ticket_agents returns only a redacted summary of it. Argument values are
never returned, so these tools cannot be used to read an agent's credentials.
Ticket streams
A stream groups tickets so they can be tracked together across a sprint, a release or an initiative.
| Tool | Action | Description |
|---|---|---|
list_ticket_streams |
read |
Streams, filtered by status, by name substring, or by stream id. |
get_ticket_stream |
read |
One stream's details. |
create_ticket_stream |
write |
Create a stream, optionally with a lead, members, attached tickets and dates. |
update_ticket_stream |
write |
Change any of the same fields. |
delete_ticket_stream |
write |
Delete a stream. The tickets and the user accounts it referenced are kept. |
member_emails and ticket_ids replace the current set rather than adding to it. To add one
member, pass the full list of members you want the stream to end up with.
Assets and inventory
| Tool | Action | Description |
|---|---|---|
list_assets |
attack_surface_read |
Attack-surface assets, filtered by asset_type, owner_id, search, tags. |
create_asset |
write |
Register one asset — a domain, an app, an IP address, a repository or a generic node — under an owner. |
update_asset |
write |
Change one asset's owner, location, colour, note, CIA requirements or tags. |
delete_asset |
write |
Remove one asset from the inventory. |
list_fingerprints |
attack_surface_read |
The technologies observed across the inventory. |
list_ports |
attack_surface_read |
The distinct ports observed across the inventory. |
list_protocols |
attack_surface_read |
The distinct protocols observed across the inventory. |
An asset is identified by the pair asset_type and asset_id, not by the id alone. Each asset
subtype lives in its own table, so the same number means different assets in different subtypes.
Both update_asset and delete_asset take the pair, and it is the pair list_assets returns.
create_asset is idempotent for the same owner: registering an asset that already exists there
returns the existing row with a created flag of false instead of failing. An identifier that
already exists under a different owner is rejected with a generic error, so a write cannot be
used to discover assets the key cannot see.
Deleting an asset does not delete its scans or its findings. The scan links are cleared, and the scans stay.
tags on update_asset replaces the asset's tags wholesale. It does not merge them.
list_fingerprints, list_ports and list_protocols summarise the inventory rather than list
it. Each returns the distinct values seen across the assets the key can reach, which answers
"what are we running" and "what is exposed" without paging through list_assets.
Owners
An owner is a team, and it is the boundary object-level access is granted on. When object-level access is enabled, a key sees the assets whose owner it can reach. An owner must exist before assets can be assigned to it.
| Tool | Action | Description |
|---|---|---|
list_owners |
read |
The organisation's owners, filtered by a name substring. |
create_owner |
write |
Create an owner with a name, an ownership type and an optional contact and parent. |
update_owner |
write |
Rename an owner, change its type, reassign its contact, or move it under a different parent. |
delete_owner |
write |
Delete an owner. |
ownership_type is one of rejected, acquisition, third_party_service or internal.
Warning
delete_owner refuses while the owner still has assets, and while any discovered-but-unconfirmed
candidate still points at it. Deletion never cascades — the assets are kept, and the refusal is
what keeps them. It is protecting you from what would otherwise follow: were the owner removed
while assets still hung off it, every object-level access grant made through it would go with
it, silently removing visibility of those assets for every key granted that way. Reassign or
delete the assets first, then delete the owner.
For a non-admin key under object-level access, list_owners returns the owners the key has been
granted plus their parent owners. An admin key sees every owner in the organisation. An empty
result therefore means either the organisation has no owners at all, or this key can reach none of
them.
Asset locations
A location is a labelled, optionally nested site or region that assets can be grouped under. It is a label only. It is not a scanner and it does not decide where a scan runs.
| Tool | Action | Description |
|---|---|---|
list_asset_locations |
read |
The locations configured for the organisation. |
create_asset_location |
write |
Create a location with a name, an optional address and an optional parent location. |
update_asset_location |
write |
Rename a location, change its address, or move it under a different parent. |
delete_asset_location |
write |
Delete a location. |
create_asset_location is idempotent: a location with the same name, address and parent is
returned as-is with a created flag of false.
delete_asset_location refuses while assets are still pinned to the location, and while any child
location still nests under it. Deletion never cascades: the assets and the child locations are
kept, and the refusal is what keeps them. Move or remove them first, then delete the location.
Discovered assets
Attack-surface discovery proposes candidates. A candidate sits in a review queue until someone accepts it into the inventory or rejects it.
| Tool | Action | Description |
|---|---|---|
list_autodiscovery_assets |
attack_surface_audit |
The pending discovery queue, filtered by key pattern, candidate type, score range and owner. |
accept_autodiscovery_asset |
attack_surface_audit |
Promote one candidate into the inventory. |
reject_autodiscovery_asset |
attack_surface_audit |
Dismiss one candidate. |
assign_potential_node_owner |
attack_surface_audit |
Assign pending candidates to an owner. |
compute_potential_assets |
attack_surface_audit |
Recompute the queue now instead of waiting for the next run. |
suggest_autodiscovery_domains |
attack_surface_audit |
Suggest the domains a company owns, from a free-text description of it. |
get_attack_surface_graph |
attack_surface_read |
The graph neighbourhood around one or more starting points. |
Warning
Accepting a candidate makes the asset scannable and billable — it joins the inventory, becomes a scan target, and counts against the organisation's plan. Both accept and reject take a single explicit id and have no bulk or filter form, on purpose, so a model cannot sweep the whole queue into the inventory in one call. Review each candidate.
Rejecting a candidate whose key has no matching inventory asset also records the key so later discovery runs stop proposing it. Rejecting a candidate whose key already matches an existing asset only drops it from the queue, and a later discovery run may propose it again.
assign_potential_node_owner sets the owner a candidate will land under before it is
accepted, so the asset arrives in the inventory already attributed to a team. It does not accept
the candidate — it stays in the queue until accept_autodiscovery_asset is called on it.
get_attack_surface_graph answers "what is this connected to". You give it one or more starting
points and it walks outward from them, returning the neighbourhood it reaches as two flat lists —
the nodes, and the edges linking them. The walk is bounded, so a starting point in a dense part of
the graph returns a capped neighbourhood rather than everything reachable from it.
compute_potential_assets runs discovery on demand rather than waiting for the next scheduled
run. Calling it while a run is already going joins that run instead of starting a second one, so
calling it twice is harmless.
Discovery configuration
Discovery is steered by a prompt describing what the organisation owns, so the candidates it proposes are relevant to you rather than to any company with a similar name.
| Tool | Action | Description |
|---|---|---|
get_attack_surface_agent_config |
read |
The prompt that steers discovery for the organisation. |
create_attack_surface_agent_config |
write |
Set that prompt. |
update_attack_surface_agent_config |
write |
Change it. |
delete_attack_surface_agent_config |
write |
Remove it. |
suggest_autodiscovery_domains is the companion to these: it turns a free-text description of a
company into a list of domains it probably owns, which is a way to draft the prompt rather than
write it from nothing. It only suggests — nothing is added to the queue or the inventory by
calling it.
Tags
A tag is a name and value pair, so the same name can exist several times with different values
— env=prod and env=staging are two tags. Pass a value to create one of those. Omit it for a
plain label.
| Tool | Action | Description |
|---|---|---|
list_tags |
attack_surface_read |
The tags defined in the organisation, filtered by a name substring. |
list_asset_tags |
attack_surface_read |
The tags actually in use on the assets the key can reach. |
add_tag |
attack_surface_audit |
Create a tag, with an optional colour, description and icon. |
update_tag |
attack_surface_audit |
Rename or restyle a tag. |
delete_tags |
attack_surface_audit |
Delete tags by id. |
delete_unused_tags |
attack_surface_audit |
Delete every tag nothing references. |
Tag names are specific to an organisation, so a guessed name matches nothing rather than failing.
Call list_tags before you filter by a tag or attach one.
update_tag changes the tag everywhere it is used, so every ticket and asset carrying it shows the
new name. It is not a way to move some items onto a different tag. Renaming a tag onto a
name/value pair another tag already holds is refused, so nothing is merged behind your back.
delete_tags removes the tag from every ticket and asset that carried it. Those tickets and assets
are kept — only the tag link goes. The call is all-or-nothing: if any id is unknown or unreachable,
nothing is deleted and the error names those ids.
list_tags returns every tag defined in the organisation, including ones nothing uses.
list_asset_tags returns only the tags in use on assets the key can reach, which is the shorter
list to pick a filter from.
delete_unused_tags takes no ids. It deletes every tag that no ticket and no asset references, in
one call, and that cannot be undone. Compare list_tags against list_asset_tags first so you
know what it will remove.
Test credentials
A test credential is a reusable login the platform can use to authenticate during a scan.
| Tool | Action | Description |
|---|---|---|
list_test_credentials |
read |
The stored credentials, filtered by type and by a substring of the non-secret fields. |
create_test_credential |
write |
Store a new credential of one of the supported types. |
update_test_credential |
write |
Replace a stored credential's values, keeping its id. |
delete_test_credential |
write |
Delete a credential. |
Stored secrets are never returned. list_test_credentials gives you the non-secret identifiers and
a has_secret flag per row.
update_test_credential is a full replacement, not a merge. Every field of the credential type
that you leave out is cleared, including the credential_name label. Pass the current value of
anything you want to keep. Its credential_type must match the type the credential was created
with.
Prefer update_test_credential over delete-and-recreate when you are rotating a value: the id keeps
resolving, so scans already configured to use that credential keep authenticating. Deleting a
credential makes those scans start failing authentication.
Scheduled scans
A schedule rule fires scans on a recurring cadence, which is how coverage stays current between one-off scans.
| Tool | Action | Description |
|---|---|---|
list_schedule_rules |
read |
The organisation's schedule rules, filtered by whether they are enabled. |
create_schedule_rule |
write |
Create a rule over one or more assets, with a scan profile and a cadence. |
update_schedule_rule |
write |
Change a rule's assets, profile, cadence or enabled state. |
delete_schedule_rule |
write |
Delete a rule. Not reversible. |
The cadence is one of two kinds. With cadence_type set to cron, crontab is a standard
five-field cron expression such as 0 16 * * * for every day at 16:00. With cadence_type set to
continuous, max_no_scan_duration_seconds is the longest gap allowed between two scans of the
rule's assets.
Warning
A rule is created disabled by default, because an enabled rule fires recurring billable
scans. Leave active at its default, check the rule with list_schedule_rules, and only then
enable it. Pass active=True on creation only when you mean the rule to start firing straight
away.
update_schedule_rule is how you enable a rule once you have checked it, by passing
active=True. It is also how you switch one off again without losing it. delete_schedule_rule
removes the rule for good; the scans it already fired are kept.
Automation rules
An automation rule runs one action whenever findings match a condition. It is how auto-ticketing and notification routing are wired up.
| Tool | Action | Description |
|---|---|---|
list_automation_rules |
read |
The organisation's shared rules, filtered by context and by whether they are enabled. |
list_action_definitions |
read |
The actions a rule can run, and the arguments each one expects. |
update_automation_rule |
write |
Change a rule's name, condition, action, context or sharing, or switch it off. |
delete_automation_rule |
write |
Delete a rule. |
create_automation_rule |
admin |
Create a rule from a context, a filter query and a single action. |
context is one of attack_surface, remediation or inventory. filter_query is the condition,
written as a JSON array, and it must contain at least one entry — an empty array applies no filter,
so the rule would match every row in its context.
Like a schedule rule, a new automation rule is created disabled. Confirm the condition and the
action, then enable it with update_automation_rule.
To switch a rule off but keep it, call update_automation_rule with active=False. That is
reversible. delete_automation_rule is not.
Only rules shared with the whole organisation are visible to an API key. Rules a person keeps private to themselves are not returned.
The action a rule runs must be one the platform defines. Call list_action_definitions before
create_automation_rule to see which actions exist and what each one takes, rather than guessing
an action name and reading the failure.
Scanners
A scanner is a machine of your own that runs scans, so scan traffic leaves your network instead of Ostorlab's. A scanner group collects several of them so a scan can be pointed at the group rather than at one machine.
| Tool | Action | Description |
|---|---|---|
list_scanners |
read |
The scanners the organisation can use. |
list_scanner_groups |
read |
The scanner groups defined in the organisation. |
create_scanner |
admin |
Register a scanner so a scan can be pinned to it. |
update_scanner |
admin |
Change a scanner's name, description or group membership. |
delete_scanner |
admin |
Retire a scanner. Not reversible. |
create_scanner_group |
admin |
Create a scanner group. |
update_scanner_group |
admin |
Change a group's name, description or member scanners. |
delete_scanner_group |
admin |
Delete a scanner group. |
grant_scanner_access |
admin |
Let child organisations run scans on one of your scanners. |
revoke_scanner_access |
admin |
Take that access away. |
grant_scanner_access and revoke_scanner_access only reach organisations that are children of
yours, and the scanner must be one your organisation owns. The grant is all-or-nothing: a list
naming an organisation that is not a child of yours is rejected before anything is written, so a
mixed list never grants in part.
Integrations
| Tool | Action | Description |
|---|---|---|
list_integrations |
read |
The third-party integrations the organisation has connected, with their enabled state and non-secret target. |
get_jira_ticket_map |
read |
Whether a ticket was pushed to Jira, and which Jira issue it became. |
configure_jira_integration |
write |
Set or replace the Jira workspace URL, credentials and default project. |
configure_slack_integration |
write |
Add a Slack webhook and choose which scan-completion events notify on it. |
create_servicenow_ticket |
write |
Push one ticket to ServiceNow as an incident. |
get_slack_integration |
admin |
Read the Slack webhooks configured, and their notification toggles. |
delete_git_pat_integration |
admin |
Delete a Git personal access token integration. |
delete_servicenow_sync_config |
admin |
Delete a ServiceNow sync configuration. |
Call list_integrations before anything that depends on an integration — pushing a ticket, sending
a notification, or naming a provider in an answer. It tells you which providers are connected, so
you do not have to attempt the action and read a failure to find out. An integration reported as
enabled may still have expired or revoked credentials; these tools cannot tell you whether it
currently works.
Credentials are never returned by any of these tools. That includes Jira API tokens, OAuth secrets, passwords, and Slack incoming-webhook URLs. Only allow-listed non-secret fields come back.
The two configure tools behave differently on a repeat call. Jira is one configuration per
organisation, so calling configure_jira_integration again updates the same row and replaces the
previous credentials. Slack is not, so calling configure_slack_integration again adds a second
webhook rather than changing the first.
Warning
delete_git_pat_integration stops source-code scans and automated fix pull requests that relied
on it. The stored token is removed from Ostorlab but stays valid at the provider, so revoke it
at the provider separately.
delete_servicenow_sync_config also destroys the record of which tickets were already pushed
under that configuration. The ServiceNow incidents themselves stay, but the link from each
Ostorlab ticket to its incident number is lost, so an already-pushed ticket can no longer be
traced or updated. Deleting the last enabled sync configuration stops tickets reaching
ServiceNow entirely.
The table above is the short list. The sections below cover the rest of the tools for each provider: the tables there add to that list rather than repeat it.
Webhooks
A webhook integration is a URL of yours that Ostorlab posts events to.
| Tool | Action | Description |
|---|---|---|
create_webhook_integration |
admin |
Set where Ostorlab posts event notifications. |
update_webhook_integration |
admin |
Change a webhook's endpoint, headers or enabled state. |
delete_webhook_integration |
admin |
Delete a webhook integration. |
Slack
| Tool | Action | Description |
|---|---|---|
update_slack_integration |
admin |
Change one Slack webhook's URL or its notification toggles. |
delete_slack_integration |
admin |
Delete one Slack webhook. |
Because configure_slack_integration adds a webhook rather than replacing the previous one,
update_slack_integration is how an existing webhook is changed. Both it and
delete_slack_integration take an id, which is the id get_slack_integration returns.
Jira
| Tool | Action | Description |
|---|---|---|
get_jira_integration |
admin |
The stored Jira configuration, without its credentials. |
test_jira_integration |
admin |
Run the Jira checks and report each result by name. |
create_jira_ticket_map |
write |
Record that a ticket corresponds to a Jira issue that already exists. |
update_jira_ticket_map |
write |
Repoint a ticket's Jira mapping at another issue. |
delete_jira_ticket_map |
write |
Detach a ticket from its Jira issue. |
create_jira_ticket |
write |
File a new Jira issue for a ticket and record the mapping. |
list_jira_metadata |
admin |
The projects, issue types and fields the stored credentials can see. |
A ticket map is the link between one Ostorlab ticket and one Jira issue, and get_jira_ticket_map
reads it. The three map write tools maintain that link by hand, for issues filed in Jira outside
Ostorlab. They change the link only. No Jira issue is created, edited or deleted by any of them,
so detaching a ticket leaves the Jira issue standing.
create_jira_ticket is the one that does reach Jira. It files a new issue for the ticket and
records the mapping in the same call, so it is the tool to use when the Jira issue does not exist
yet. A failure leaves nothing behind — no issue and no mapping — and names what was wrong, so a
project the credentials cannot see is reported as such rather than as a generic error.
list_jira_metadata is how you find the values create_jira_ticket and
configure_jira_integration need: the projects the credentials can reach, the issue types each
project accepts, and the fields available for mapping. An empty result means the credentials see
no project, which is worth checking with test_jira_integration before assuming the tool is at
fault.
test_jira_integration reports each check by name, so a failure tells you which part is wrong —
the URL, the credentials or the project — rather than only that something is.
Linear
| Tool | Action | Description |
|---|---|---|
configure_linear_integration |
admin |
Set or replace the Linear connection. |
get_linear_integration |
admin |
The stored Linear configuration, without its credentials. |
test_linear_integration |
admin |
Report whether Linear is set up, enabled and reachable. |
list_linear_teams |
admin |
The Linear teams issues can be filed against. |
ServiceNow
| Tool | Action | Description |
|---|---|---|
configure_servicenow_integration |
admin |
Set or replace the ServiceNow connection. |
test_servicenow_integration |
admin |
Run the checks of one ServiceNow configuration and report each result by name. |
delete_servicenow_integration |
admin |
Delete a ServiceNow integration. |
configure_servicenow_sync_config |
admin |
Map tickets onto a ServiceNow table and its fields. |
get_servicenow_sync_config |
read |
How one sync configuration maps tickets to a table. |
list_servicenow_fields |
read |
The columns of the ServiceNow table a sync configuration targets. |
get_servicenow_ticket_map |
read |
Whether a ticket was pushed to ServiceNow, and which record it became. |
create_servicenow_ticket_map |
write |
Record that a ticket maps to a ServiceNow record that already exists. |
update_servicenow_ticket_map |
write |
Repoint a ticket's ServiceNow mapping at another record. |
delete_servicenow_ticket_map |
write |
Detach a ticket from its ServiceNow record. |
ServiceNow is set up in two steps. configure_servicenow_integration stores the instance and its
credentials. configure_servicenow_sync_config then says which table tickets are written to and
which column each ticket field maps onto. Call list_servicenow_fields first to see the columns
that table actually has, so the mapping names real columns.
The ticket-map tools work the same way as the Jira ones: they maintain the link between an
Ostorlab ticket and a ServiceNow record, and never touch the record itself. create_servicenow_ticket
is the tool that does create a record.
Source code
| Tool | Action | Description |
|---|---|---|
list_repositories |
read |
The repositories Ostorlab can currently reach. |
create_git_pat_integration |
admin |
Register a Git personal access token. |
update_git_pat_integration |
admin |
Replace a stored token or change what it covers. |
create_standard_git_integration |
admin |
Register a self-hosted Git instance. |
update_standard_git_integration |
admin |
Change a self-hosted Git registration. |
delete_standard_git_integration |
admin |
Delete a self-hosted Git registration. |
enable_github_app_connection |
admin |
Resume source-code scans and pull requests for a GitHub App's repositories. |
disable_github_app_connection |
admin |
Stop them, keeping the installation in place. |
delete_github_app_connection |
admin |
Remove a GitHub App installation from the organisation. |
update_source_code_oauth_integration |
admin |
Activate or deactivate a source-code OAuth integration. |
delete_source_code_oauth_integration |
admin |
Delete a source-code OAuth integration. |
list_repositories is the tool to call before create_source_code_scan. It returns the
repositories the connected source-code integrations currently expose, so you scan something the
platform can actually clone rather than a name you guessed.
disable_github_app_connection is the reversible form of delete_github_app_connection.
Disabling stops scans and pull requests but keeps the installation, so
enable_github_app_connection puts it back as it was. Deleting removes the installation, and
getting it back means going through GitHub to install the app again.
App Center
| Tool | Action | Description |
|---|---|---|
create_app_center_integration |
admin |
Connect an App Center app so its builds are scanned automatically. |
update_app_center_integration |
admin |
Change an App Center integration. |
delete_app_center_integration |
admin |
Delete an App Center integration. |
Vanta
| Tool | Action | Description |
|---|---|---|
get_vanta_auth_url |
admin |
The link to open in a browser to authorise Ostorlab with Vanta. |
delete_vanta_integration |
write |
Disconnect Vanta. |
Connecting Vanta needs a browser. get_vanta_auth_url returns the link a person opens to
authorise Ostorlab, and the connection is completed there rather than through a tool, so a client
cannot finish this one on its own.
Organisation and access
| Tool | Action | Description |
|---|---|---|
me |
none beyond a valid key | The organisation the key belongs to, and the role the key carries. |
list_shared_access_tokens |
read |
The organisation's active share links, with the scan each grants and how often it has been used. |
list_invitations |
read |
The people invited to the organisation who have not joined yet. |
create_shared_access_token |
write |
Create a link that lets someone without an account read one scan. |
revoke_shared_access_token |
write |
Disable a share link immediately. |
list_organisation_users |
admin |
Who has access to the organisation, at what role, and scoped to which owners. |
add_user |
admin |
Invite someone to join the organisation at a given role. |
update_user_access |
admin |
Change a member's role, or the owners they are scoped to. |
revoke_user |
admin |
Remove someone's access to the organisation. |
review_invitation |
admin |
Accept or decline a pending invitation. |
get_organisation_settings |
admin |
The organisation's settings and entitlements. |
update_organisation_settings |
admin |
Change the settings that can be written. |
list_api_keys |
admin |
The organisation's API keys: name, role, created and expiry dates, revoked state, and a masked form of the key. |
update_api_key |
admin |
Narrow a key: lower its role, drop owner grants, bring its expiry forward, or rename it. |
revoke_api_key |
admin |
Disable an API key immediately. |
list_audit_actions |
admin |
Who changed what, and when, filtered by time range and by actor. |
me describes the key, not a person. The server authenticates a key, so the answer to "who am I"
is the organisation the key belongs to and the key's own name, prefix, role, owner grants and
expiry. No user account is returned.
add_user sends an invitation rather than creating an account. The person shows up in
list_invitations until they accept, and only then in list_organisation_users.
update_api_key can only narrow a key. The role it sets must be no higher than the role the key
already has, owner grants can be dropped but not added, and an expiry can only be brought forward.
So it is not a way to give a key more than it has, and it never returns key material.
Revoking is the only thing that disables an API key. An expiry date on its own does not — the key
keeps working on the MCP path until revoked is set. revoke_api_key sets it, and the change
cannot be undone. A key cannot revoke itself; that call is refused, since it would cut your own
connection mid-session.
The plaintext of an API key is never returned by any tool. It exists only at the moment the key is
created, and only its hash is stored afterwards. list_api_keys returns a masked_key instead —
the key's prefix followed by asterisks, the same form the web application and the audit trail show.
Use that when you need to name a key to a person.
Warning
Anyone holding a shared access token can read the scan it points at, and its findings, without
an account. These tokens do not expire. list_shared_access_tokens lists only the shares that
are live right now, so every row is access someone currently has. The raw token comes back only
once, in the response to create_shared_access_token.
list_audit_actions returns an empty list rather than an error when the organisation has action
auditing switched off. The response carries a message field saying so, so that "no rows" is not
read as "nothing happened".
Single sign-on
| Tool | Action | Description |
|---|---|---|
get_saml_config |
admin |
The organisation's SAML single sign-on configuration. |
configure_saml |
admin |
Set SAML single sign-on up. |
update_saml |
admin |
Change the SAML configuration. |
configure_saml is a strict create: it is refused when the organisation already has a
configuration, and it never replaces one. update_saml is the tool for an existing configuration,
and it is a partial update — only the fields you supply are written. Call get_saml_config first
to see which of the two applies.
Remediation SLOs
An SLO here is a deadline: the number of days a ticket of a given severity or priority is allowed to stay open.
| Tool | Action | Description |
|---|---|---|
get_slo_config |
read |
The organisation's per-severity and per-priority windows, in days. |
update_slo_config |
write |
Change those windows. |
A window of null means there is no deadline for that severity or priority. update_slo_config is
a partial update: a key you include with a positive number sets that window, a key you include as
null clears it, and a key you leave out is unchanged. An unknown key is rejected.
UI automation prompts
| Tool | Action | Description |
|---|---|---|
list_ui_automation_rules |
read |
The UI automation rules available to the organisation. |
create_ui_prompts |
write |
Create UI automation prompts. |
update_ui_prompts |
write |
Change the code, name or description of UI automation prompts your organisation owns. |
delete_ui_prompts |
write |
Delete UI automation prompts your organisation owns. Not reversible. |
code is a full replace and is written on every call, while name and description are written
only when you supply them. So a call that only means to rename a prompt must still send the
prompt's existing code, or the code is overwritten with whatever was sent.
The call is not all-or-nothing. Prompts that could be reached are updated even when others could not. The response names the ids that were updated and, for each of the rest, why it was not.
list_ui_automation_rules also returns rules the organisation does not own, which are the shared
ones every organisation can use. Only the ones your organisation owns can be changed or deleted,
so a prompt that lists fine can still be refused by update_ui_prompts and delete_ui_prompts.
Knowledge base
The knowledge base is Ostorlab's own reference data. It is the same for every organisation rather than a view of yours, so these tools return rows that have nothing to do with your scans.
| Tool | Action | Description |
|---|---|---|
list_cves |
read |
CVE records, looked up by id, by severity, or by text in the description. |
list_kb_applications |
read |
The public applications Ostorlab watches for newly published CVEs. |
list_kb_targets |
read |
The rows linking a CVE to the application versions it affects. |
list_app_cards |
read |
The shared App Cards available in Ostorlab. |
list_kb_targets is the join between the other two: given a CVE it names the affected application
versions, and given an application it names the CVEs published against it.
Agent store
A scan profile is built out of agents. The store is the catalogue of agents that exist.
| Tool | Action | Description |
|---|---|---|
search_agents |
read |
Search the agent store by name. |
get_agent |
read |
One agent, with the arguments its latest version takes. |
search_agents is the discovery step and get_agent is the detail step: search by name to find
the agent's key, then read the arguments that key accepts.
Exports
Each of these builds a file and returns a link to it, not the content itself. Fetch that URL over
HTTPS with the same API key in an X-API-Key header, the same way generate_scan_report works.
| Tool | Action | Description |
|---|---|---|
export_scan |
read |
The full archive of one scan. |
export_scan_sarif |
read |
One scan's findings as SARIF. |
export_vulnerabilities |
read |
One scan's findings as CSV. |
export_tickets |
read |
Remediation tickets as CSV. |
export_assets |
attack_surface_read |
The confirmed inventory assets as CSV. |
export_potential_nodes |
attack_surface_read |
The pending discovery candidates as CSV. |
The two attack-surface exports need attack_surface_read, which every role holds. The other four
need read, so an attack_surface_auditor key can export assets and discovery candidates but not
scans, findings or tickets.
Result limits
Every list tool caps how much it returns. What a tool tells you about that cap is not the same everywhere. There are three behaviours.
Some list tools return a cursor. A cursor is an opaque token that marks the point where the
page you just received ended. It comes back in a next_cursor field. Pass that value back as the
cursor argument on the next call and you get the following page. A next_cursor of null
means you have reached the last page. Do not read a cursor or build one yourself — what is inside
it is an implementation detail, and it only means anything to the tool that produced it.
Some list tools return a truncated boolean. It tells you that more rows exist, but it gives
you no way to fetch them. Narrow the query with filters instead.
A few return neither. For those the cap is silent: you get the capped number of rows, and nothing tells you that more exist.
| Tool | Default | Maximum | How more rows are signalled |
|---|---|---|---|
list_scans |
200 | 200 | next_cursor |
list_vulnerabilities |
200 | 200 | next_cursor |
list_assets |
200 | 200 | next_cursor |
list_risks |
200 | 200 | next_cursor |
get_scan_status (status history) |
50 | 50 | nothing |
list_scan_profiles |
no limit | no limit | not applicable |
list_checklists |
no limit | no limit | not applicable |
search_vulnerabilities |
200 | 200 | truncated |
list_tags |
200 | 200 | next_cursor |
list_api_keys |
200 | 200 | truncated |
list_shared_access_tokens |
200 | 200 | truncated |
list_ticket_agents |
100 | 100 | truncated |
list_owners |
200 | 200 | next_cursor |
list_asset_locations |
200 | 200 | next_cursor |
list_autodiscovery_assets |
200 | 200 | next_cursor |
list_test_credentials |
200 | 200 | next_cursor |
list_integrations |
200 | 200 | next_cursor |
list_audit_actions |
200 | 200 | next_cursor |
list_organisation_users |
200 | 200 | next_cursor |
list_schedule_rules |
100 | 100 | next_cursor |
list_automation_rules |
100 | 100 | next_cursor |
get_slack_integration |
100 | 100 | next_cursor |
list_ticket_streams |
50 | 200 | next_cursor |
list_tickets |
50 | 200 | truncated and next_cursor |
list_comments |
50 | 200 | truncated and next_cursor |
Asking for more than the maximum is clamped down to it rather than rejected. Asking for zero, a
negative number, or something that is not a number is rejected with Invalid limit. A cursor is
tied to the filters it was produced under, so replaying one under different filters is rejected
with Invalid cursor. rather than quietly returning the wrong page.
Where a tool signals nothing, the cap is the whole answer you get: a scan with 500 status entries
returns 50 from get_scan_status, with nothing to indicate the other 450 exist. Narrow with
filters when the count matters — do not ask a model to count from a list result.
Deleting
Every delete_* and revoke_* tool on this page removes data permanently. That includes
delete_ticket, delete_comment, delete_checklist and delete_checklist_item, and equally
delete_asset, delete_owner, delete_asset_location, delete_tags, delete_unused_tags,
delete_test_credential, delete_scanner, delete_scanner_group, delete_schedule_rule,
delete_automation_rule, delete_ui_prompts, delete_ticket_agent, delete_ticket_stream,
delete_checklist, the integration deletes, and revoke_api_key,
revoke_shared_access_token and revoke_user.
Some of them refuse rather than cascade. delete_owner and delete_asset_location will not run
while something still depends on them, and delete_asset keeps the scans and findings that were
attached to the asset. Where a tool cascades or keeps data, the section describing it says so.
Tools advertise annotations to the client. An annotation is a hint that travels with the tool in the tool list and describes what calling it does, so the client can decide whether to ask you first. Three hints are used:
readOnlyHint— the tool changes nothing.destructiveHint— the tool changes state in a way that cannot be undone.idempotentHint— calling the tool twice has the same effect as calling it once.
Those tools are annotated as destructive, and so is every other tool that removes or overwrites data.
Warning
An annotation is a hint, not enforcement. Whether you are actually asked to confirm is up to your client. Some clients prompt every time a destructive tool is called, some prompt once and remember the answer, and some do not prompt at all. The server runs the call either way. Check how your client handles destructive hints before you point a model at a key that can write.
Not every tool carries annotations. A tool that carries none leaves the client to fall back on the MCP specification's default, which assumes the call is destructive and not idempotent. That means an unannotated read tool can look more dangerous to a cautious client than it really is.
Errors
A tool that fails returns a normal successful response whose body carries an error field:
{"error": "Invalid API Key."}
The JSON-RPC isError flag stays false. If your client branches on isError, it will read
authentication and permission failures as successes — check for the error field instead.
Examples
Prompts that work well once the server is connected:
- "List the scans that finished in the last week with a high risk rating."
- "Show me the critical findings from scan 12345 and open a ticket for each one."
- "What's the technical detail of vulnerability 987, and has anyone commented on its ticket?"
- "Mark vulnerability 654 as a false positive."
- "Which open tickets are assigned to rubberduck?"
- "Which scan profiles can we run against a web target, and start a Fast Scan on example.com."
- "Which of our API keys have never been revoked and were created more than a year ago?"
- "What is sitting in the attack-surface discovery queue with a score above 80?"
Troubleshooting
HTTP 404 on connect
The URL is missing the key segment, or the trailing slash. It must be
/apis/mcp/YOUR_API_KEY/.
Invalid API Key.
The key does not resolve. Check it was copied whole, and that it has not been revoked.
API Key does not have write permission.
The tool needs a higher role than the key has. Issue a key with the required role.
Scan not found. for a scan you can see in the web application
Four possible causes: the scan is archived (get_scan and get_scan_status refuse archived
scans), the scan belongs to another organisation, your organisation uses object-level access and
this key has not been granted that scan, or the id is an AI pentest rather than a scan — the
scan tools do not resolve AI pentest ids, so use get_agentic_deep_scan for those. An
inaccessible scan is deliberately reported the same way as one that does not exist.