Conventions shared by every tool¶
The call result¶
A tool returns a JSON object. The layer sends it twice: as structuredContent, and as pretty-printed JSON text in content[0] for clients without structured output. Resource links the tool attached follow as resource_link blocks, and _meta carries cloudg/duration_ms plus, when the policy transformed anything, a cloudg/transforms report. The full wire result of dataset_summary with {"dataset": "report"}, with the text block cut:
{
"content": [
{
"type": "text",
"text": "{\n \"dataset\": \"report\",\n \"kind\": \"report\",\n \"source\": \"<estate>/report/findings.json\",\n \"loaded_at\": \"2026-10-09T14:25:10+00:00\",\n \"version\": 0,\n \"provide..."
},
{
"type": "resource_link",
"uri": "cloudg://datasets/report/summary",
"name": "report summary",
"mimeType": "application/json"
}
],
"isError": false,
"structuredContent": {
"dataset": "report",
"kind": "report",
"source": "<estate>/report/findings.json",
"loaded_at": "2026-10-09T14:25:10+00:00",
"version": 0,
"providers": [
"aws",
"azure",
"... 1 more"
],
"total_assets": 44,
"total_edges": 56,
"total_findings": 12,
"open_findings": 11,
"suppressed_findings": 1,
"severity_breakdown": {
"CRITICAL": 2,
"HIGH": 5,
"MEDIUM": 2,
"LOW": 1,
"INFO": 1
},
"accounts": 5,
"regions": 5,
"internet_exposed": 6,
"cross_account_edges": 3,
"unlinked_assets": 2,
"unresolved_references": 0,
"compliance_frameworks": [
"CIS-AWS",
"CIS-Azure",
"... 2 more"
],
"assets_by_type": {
"SECURITY_GROUP": 4,
"IAM_ROLE": 4,
"CLOUD_ACCOUNT": 3,
"EC2": 3,
"S3_BUCKET": 3,
"...": "10 more"
},
"assets_by_provider": {
"AWS": 36,
"AZURE": 5,
"GCP": 3
},
"edges_by_type": {
"CONTAINS": 12,
"IAM_TRUST": 2,
"GOVERNS": 1,
"SECURITY_GROUP_RULE": 4,
"ATTACHED_TO": 6,
"...": "10 more"
},
"has_organization": false,
"coverage_records": 0
},
"_meta": {
"cloudg/duration_ms": 2
}
}
The text rendering is capped at max_output_chars (200,000 by default, set on CloudGMCPLayer). Past that the text ends with ... [truncated by cloudg mcp layer]; structuredContent is not cut.
The dataset argument¶
Every tool that reads data takes dataset. Empty means the active dataset, which is the one most recently loaded, selected or collected. A name that is not loaded fails; error_data holds the name and the list of loaded datasets:
{
"tool": "select_dataset",
"arguments": {
"name": "prod"
},
"isError": true,
"text": "No such dataset. 2 dataset(s) are loaded: see datasets in the error data, or call list_datasets.",
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": {
"value": "prod",
"datasets": [
{
"dataset": "sample"
},
{
"dataset": "report"
}
]
}
}
}
With nothing loaded at all, any tool that needs data fails the same way and says how to load something:
{
"tool": "dataset_summary",
"arguments": {},
"isError": true,
"text": "No dataset is loaded. Call load_dataset(path=...) with an inventory-map.json / findings.json / scanner report, or map_inventory to collect live data.",
"_meta": {
"cloudg/error_code": -32602
}
}
A few tools need no dataset: workspace_status, list_datasets, list_frameworks, list_capabilities, describe_schema, explain_asset_type, explain_edge_type and the privacy tools. Resources and resource templates always read the active dataset; they have no dataset variable except cloudg://datasets/{dataset}/summary.
Asset references¶
Arguments named ref, source, target, start, seeds, refs and resource take an asset reference. Dataset.find_asset (cloudg/mcp/state/dataset.py) resolves it with cloudg.inventory.dependencies.AssetIndex, the matcher the ontology and the RAG export use too. It tries these in order and stops at the first unique hit:
- the internal asset id (
web-1); - the exact ARN or cloud resource id (
arn:aws:ec2:us-east-1:111111111111:instance/i-0web1, an Azure resource id, a GCP full name); - an exact name shared by no other asset (
prod-data); - the text after the last
/of the ARN or resource id, when only one asset has it (i-0web1,sg-0admin; Azure and GCP ids contribute their last/segment); - the resource part of an ARN, i.e. everything after the fifth
:(function:api-handler,db:orders-db,instance/i-0web1), or its last:or/segment (api-handler,orders-db), when only one asset has it; - a case-insensitive name shared by no other asset.
A name or tail that several assets share does not resolve, and the last two steps are skipped for a reference that is the exact name of more than one asset; use a longer form or the id.
Error messages never repeat the reference you passed or the names of other assets: those go into _meta.cloudg/error_data, where the privacy policy can transform them like any other result. When a name matches several assets, error_data holds the ids in ambiguous and up to five candidates (id, name, ARN, type, account). When nothing matches, error_data.value is the reference and suggestions lists up to five close assets (substring hits on name or ARN first, then fuzzy name matches):
{
"tool": "get_asset",
"arguments": {
"ref": "web-3"
},
"isError": true,
"text": "No asset matches the reference in this dataset. 5 close matches, see suggestions in the error data. Use find_assets(query=...) to search by name, ARN or tag.",
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": {
"value": "web-3",
"dataset": "sample",
"suggestions": [
{
"id": "web-2",
"name": "web-2",
"arn": "arn:aws:ec2:us-east-1:111111111111:instance/i-0web2",
"type": "EC2",
"account_id": "111111111111",
"dataset": "sample"
},
{
"id": "web-1",
"name": "web-1",
"arn": "arn:aws:ec2:us-east-1:111111111111:instance/i-0web1",
"type": "EC2",
"account_id": "111111111111",
"dataset": "sample"
},
"... 3 more"
]
}
}
}
A Lambda function by the resource part of its ARN:
Called with the arguments below, get_asset returned this structuredContent.
{
"id": "api-handler",
"name": "api-handler",
"type": "LAMBDA_FUNCTION",
"provider": "AWS",
"region": "us-east-1",
"account_id": "111111111111",
"arn": "arn:aws:lambda:us-east-1:111111111111:function:api-handler",
"internet_exposed": false,
"open_findings": 1,
"max_severity": "LOW",
"uri": "cloudg://assets/api-handler",
"tags": {
"owner": "api-team"
},
"dataset": "sample",
"service": "lambda",
"relations": {
"outgoing": {
"ASSUMES_ROLE": 1,
"LOGS_TO": 1,
"REFERENCES": 1,
"USES_IMAGE": 1
},
"incoming": {
"INVOKES": 2
},
"sample": [
{
"id": "e-lambda-role",
"source": "api-handler",
"source_name": "api-handler",
"target": "lambda-role",
"target_name": "api-lambda-role",
"edge_type": "ASSUMES_ROLE",
"relationship": null
}
],
"total": 6,
"truncated": true
},
"dependencies": {
"direct_depends_on": 6,
"direct_dependents": 0
},
"degree": {
"in": 2,
"out": 4
},
"metadata_keys": [
"runtime"
],
"collected_at": "2026-01-15T12:00:00+00:00"
}
The result also carried these resource links in content:
[
{
"type": "resource_link",
"uri": "cloudg://assets/api-handler",
"name": "api-handler",
"title": "LAMBDA_FUNCTION api-handler",
"mimeType": "application/json"
},
{
"type": "resource_link",
"uri": "cloudg://assets/api-handler/neighbors",
"name": "api-handler neighbors",
"mimeType": "application/json"
}
]
The graph tools (neighbors, get_edges, find_paths, attack_paths, lateral_movement_paths, subgraph_export) accept two more forms. internet, public, 0.0.0.0/0 and ::/0 all mean the internet. The entry points are found the way the reachability analysis finds them: the 0.0.0.0/0 and ::/0 nodes and the source of every ingress edge whose CIDR is 0.0.0.0/0, ::/0 or an Azure Internet, Any or * tag. neighbors and subgraph_export start from the first entry point (0.0.0.0/0 when it exists); find_paths and attack_paths start from a virtual node, __internet__ (shown as internet), linked to every entry point, so an Azure-only dataset works too. Without any entry point they fail with This dataset has no internet node (no internet-sourced ingress rules or INTERNET_EXPOSED edges). Use internet_exposure to see assets flagged as exposed. Any other placeholder node in the graph (a CIDR, an external principal) can be passed by its node id.
Most results name assets by their internal id (cross_account_edges and the keys in diff_datasets use ARNs). Pass ids back when you chain calls; they are the only form that never becomes ambiguous.
Pagination¶
List tools take limit and cursor and return an envelope:
| Field | Meaning |
|---|---|
total |
Matches before paging. |
offset |
Index of the first item on this page. |
returned |
Items on this page. |
next_cursor |
Pass it back as cursor for the next page; null on the last page. |
truncated |
true when more pages exist. |
items |
The page. |
limit defaults to 50 and allows 1 to 500 unless the tool's table says otherwise. A cursor looks like c3.054790bd1d07: c, the offset of the next item, a dot, then the first 12 hex digits of a SHA-256 over the dataset name, the dataset version, an id of that particular load of the dataset and the query arguments (limit excluded, so the page size may change between pages). The format is short and dotted on purpose: the privacy policies redact long base64-looking strings as secrets, and a redacted cursor could not be passed back.
A cursor only works with the query and dataset version that produced it. Change a filter, mutate the dataset (suppress, ingest, normalise, add reachability findings, run scanners), or load a new dataset under the same name, and the old cursor fails with an invalid-arguments error instead of returning a wrong page. The first two pages of a risk-sorted search:
Called with the arguments below, find_assets returned this structuredContent.
{
"dataset": "sample",
"total": 44,
"offset": 0,
"returned": 3,
"next_cursor": "c3.054790bd1d07",
"truncated": true,
"items": [
{
"id": "sg-admin",
"name": "sg-admin",
"type": "SECURITY_GROUP",
"max_severity": "CRITICAL",
"open_findings": 1
},
{
"id": "bastion",
"name": "bastion",
"type": "EC2",
"max_severity": "HIGH",
"open_findings": 1
},
{
"id": "logs-bucket",
"name": "prod-logs",
"type": "S3_BUCKET",
"max_severity": "HIGH",
"open_findings": 1
}
]
}
Called with the arguments below, find_assets returned this structuredContent.
{
"sort_by": "risk",
"limit": 3,
"cursor": "c3.054790bd1d07",
"fields": [
"name",
"type",
"max_severity",
"open_findings"
]
}
{
"dataset": "sample",
"total": 44,
"offset": 3,
"returned": 3,
"next_cursor": "c6.054790bd1d07",
"truncated": true,
"items": [
{
"id": "az-nsg",
"name": "jump-nsg",
"type": "NSG",
"max_severity": "CRITICAL",
"open_findings": 1
},
{
"id": "gcp-gcs",
"name": "raw-uploads",
"type": "GCS_BUCKET",
"max_severity": "HIGH",
"open_findings": 1
},
{
"id": "web-1",
"name": "web-1",
"type": "EC2",
"max_severity": "HIGH",
"open_findings": 1
}
]
}
Reusing that cursor with sort_by: "name" fails, and so does anything that is not a cursor:
{
"tool": "find_assets",
"arguments": {
"sort_by": "name",
"limit": 3,
"cursor": "c3.054790bd1d07"
},
"isError": true,
"text": "Stale cursor: the query arguments or the dataset changed since it was issued. Repeat the call without cursor to start again.",
"_meta": {
"cloudg/error_code": -32602
}
}
{
"tool": "find_assets",
"arguments": {
"cursor": "eyJvZmZzZXQiOjN9"
},
"isError": true,
"text": "Invalid cursor. Pass the next_cursor value from the previous result unchanged, or omit cursor to start from the first page.",
"_meta": {
"cloudg/error_code": -32602
}
}
Paginated with a cursor: find_assets, list_accounts, unresolved_references, get_edges, internet_exposure, cross_account_edges, list_findings, list_controls, compliance_gaps, relation_groups (with group) and rag_chunks.
Other list tools return the first limit or top items with total, returned and truncated and no cursor: list_tags, depends_on, dependents, findings_for_asset, reachability_findings, the three lists of security_coverage, control_status, and the graph_reachable block of internet_exposure. attack_paths and lateral_movement_paths report total_found and truncated. count_assets and findings_summary report group_count or truncated for their groups.
Field projection¶
find_assets and list_findings take fields, a list of keys to keep in each item. id is always kept and comes first. Unknown names fail before any work is done:
{
"tool": "find_assets",
"arguments": {
"fields": [
"owner"
]
},
"isError": true,
"text": "Unknown field(s) owner. Valid fields: id, name, type, provider, region, account_id, arn, internet_exposed, open_findings, max_severity, tags, uri",
"_meta": {
"cloudg/error_code": -32602
}
}
| Tool | Valid fields |
|---|---|
find_assets |
id, name, type, provider, region, account_id, arn, internet_exposed, open_findings, max_severity, tags, uri |
list_findings |
id, title, severity, risk_score, source_tool, resource_id, resource_arn, asset_id, asset_name, compliance_frameworks, is_suppressed, cvss_score, detected_at, uri |
tags is the only asset field that is off by default. find_assets adds it to each item only when fields asks for it.
Sorting¶
| Tool | Order |
|---|---|
find_assets |
sort_by: name (case-insensitive, then id), type, region, account, risk, findings. risk and findings start with the highest; descending: true reverses whichever order applies. |
list_findings |
sort_by: risk (risk_score, then severity, then id, highest first), severity (severity, then risk_score), detected_at (newest first), title, source_tool (then risk_score). |
internet_exposure |
Most sensitive ports open first, then no WAF before WAF, then most open findings, then name. |
findings_for_asset |
risk_score, highest first. |
top_risks |
Score, highest first, then asset name. |
attack_paths |
Path score, highest first, then shortest. |
lateral_movement_paths |
Highest crown-jewel weight on the chain first, then shortest. |
find_paths |
Shortest first. |
list_controls (source: "dataset") |
Worst open severity, then most open findings, then control id. |
compliance_gaps |
Worst gap severity, then internet-exposed first, then most gap findings, then name. |
count_assets, findings_summary |
Largest group first (ties by key). findings_summary with group_by: "severity" uses severity order instead. |
list_accounts, list_regions, list_asset_types, list_tags |
Most assets first. |
centrality_top |
Score, highest first. |
Severity¶
Severities rank CRITICAL (4) > HIGH (3) > MEDIUM (2) > LOW (1) > INFO (0). min_severity keeps items at or above the given level. Severity arguments are case-insensitive and also accept - or spaces for _. An unknown value fails with the valid list:
{
"tool": "list_findings",
"arguments": {
"min_severity": "SEVERE"
},
"isError": true,
"text": "Unknown severity. Valid values: CRITICAL, HIGH, MEDIUM, LOW, INFO",
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": {
"value": "SEVERE",
"valid": [
"CRITICAL",
"HIGH",
"MEDIUM",
"LOW",
"INFO"
],
"close_matches": []
}
}
}
max_severity in an asset brief is the worst severity among the asset's open (not suppressed) findings, or null. count_assets with group_by: "severity" reports assets without open findings under NONE.
Enumerated arguments¶
Asset types, edge types, providers and relation groups are parsed the same way as severities: case-insensitive, with - and spaces read as _. A miss names up to three close values in the message and lists the valid ones (the first 30 when there are more). error_data holds the value that was passed (value), every valid value (valid) and the close matches (close_matches):
{
"tool": "neighbors",
"arguments": {
"ref": "web-1",
"edge_types": [
"ASSUMES"
]
},
"isError": true,
"text": "Unknown edge type. Did you mean ASSUMES_ROLE? Valid values: SECURITY_GROUP_RULE, NACL_RULE, ROUTE, IAM_TRUST, IAM_POLICY_ATTACHMENT, CONTAINS, PEERING, LOAD_BALANCER_TARGET, INTERNET_EXPOSED, ATTACHED...",
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": {
"value": "ASSUMES",
"valid": [
"SECURITY_GROUP_RULE",
"NACL_RULE",
"ROUTE",
"... 17 more"
],
"close_matches": [
"ASSUMES_ROLE"
]
}
}
}
The valid values come from describe_schema, list_asset_types (only the types present in a dataset) or the cloudg://schema/* resources.
Briefs¶
Most tools describe assets, edges and findings with the same compact objects.
An asset brief (asset_brief in cloudg/mcp/catalog/_common.py):
| Field | Meaning |
|---|---|
id |
Internal asset id. |
name |
Display name. |
type |
AssetType value. |
provider |
AWS, AZURE or GCP. |
region |
Region, or global. |
account_id |
Account, subscription or project. |
arn |
ARN or cloud resource id; may be null. |
internet_exposed |
The collector's is_internet_exposed flag. |
open_findings |
Findings matched to the asset that are not suppressed. |
max_severity |
Worst severity among them, or null. |
uri |
cloudg://assets/{id}, readable as a resource. |
tags |
Only where noted (get_asset, find_assets with fields). |
Path tools use a shorter hop brief with id, name, type, account_id, internet_exposed, open_findings and max_severity; null values are left out. Graph placeholders (the internet node, a CIDR, an external id) appear as {"id", "name", "type": "EXTERNAL", "external": true}.
An edge brief:
| Field | Meaning |
|---|---|
id |
Edge id. |
source, target |
Endpoint ids. |
source_name, target_name |
Asset names (the id when the endpoint is not an asset). |
edge_type |
EdgeType value. |
relationship |
Semantic relationship (CROSS_ACCOUNT_TRUST, ENCRYPTED_BY_KMS...) or null. |
port_range, protocol, cidr, description |
Present only when set. |
ports |
Explicit ports, first 20, present only when set. |
direction |
ingress or egress, only on SECURITY_GROUP_RULE and NACL_RULE edges. |
cross_account |
true when both ends are assets in different accounts; absent otherwise. |
A finding brief:
| Field | Meaning |
|---|---|
id |
Finding id. |
title, severity, source_tool |
As reported. |
risk_score |
0 to 10, see Risk scores. |
resource_id, resource_arn |
The resource as the scanner named it. |
asset_id, asset_name |
The mapped asset, or null when the resource matched nothing. |
compliance_frameworks |
Framework tags on the finding. |
is_suppressed |
Suppression flag. |
cvss_score |
CVSS, or null. |
detected_at |
ISO 8601 timestamp. |
uri |
cloudg://findings/{id}. |
A finding is matched to an asset by its resource_id and resource_arn, with the same AssetIndex tiers as an asset reference: id, then ARN, unique name, unique ARN tail, ARN resource part, and a name in any letter case. A name or tail several assets share matches nothing. Findings that match nothing count as unmapped in findings_summary and show asset_id: null.
Risk scores¶
Four different scores appear in results:
risk_scoreon a finding is computed by theFindingmodel: CRITICAL 9.5, HIGH 7.5, MEDIUM 5.0, LOW 2.5, INFO 0.5; with a CVSS score it is the mean of that value and the CVSS, rounded to one decimal.f-web-cve(HIGH, CVSS 8.1) scores 7.8.find_assetswithsort_by: "risk"ranks on the worst open finding'srisk_score, plus 2.0 when the asset is internet-exposed, then the number of open findings.top_risksscores an asset asmax_finding_risk * exposure * blast * volume.exposureis 1.5 when the asset is flagged internet-exposed or the reachability analysis reaches it from the internet over network-flow edges (internet_reachable), else 1.0.blastis1 + min(1, log10(1 + d) / 2)wheredcounts transitive dependents up to depth 6.volumeis1 + min(0.5, 0.05 * (open findings - 1)). The result is rounded to two decimals, and each item carries the components.attack_pathsscores a path astarget weight + 0.5 * min(10, open findings on the path) - 0.3 * (hops - 1). Target weights come from the crown-jewel table: relational databases 10; DynamoDB, data warehouses, secrets and key vaults 9; KMS keys and buckets 8; file systems and search domains 7; caches and access keys 6; IAM roles and users 5. An explicittargetalways weighs 10.blast_radius.network.risk_scorecomes fromReachabilityAnalyzer.compute_blast_radius: 0.5 per reachable node plus 2.0 per reachable sensitive data store, capped at 10.
Sensitivity, capabilities and policies¶
Each primitive declares a sensitivity and the side effects it needs. Policies use both to decide whether a caller sees the primitive and what transforms its output goes through (MCP_PRIVACY.md has the details).
| Sensitivity | Used for |
|---|---|
public |
Vocabulary and catalog data with no tenant information (meta tools, list_frameworks, list_detectors, schema and docs resources). |
internal |
Counts and summaries. |
confidential |
Identifiers, topology and findings. Most tools. |
restricted |
Raw metadata and anything that may hold secrets: get_asset_metadata, privacy_audit_log, reveal_token. Also the outputs the privacy layer cannot look inside, because they are one opaque string: sparql_query (its rows are keyed by the query's own variable names), subgraph_export and the text graph and ontology resources (cloudg://graph/{format}, cloudg://ontology/turtle, cloudg://ontology/{format}). A profile with a confidential ceiling hides all of them. |
| Capability | Meaning | Tools |
|---|---|---|
read_state |
Reads loaded datasets or workspace state. | Default for every tool that touches data, and rate_limit_status. |
write_state |
Changes the in-memory workspace. | load_dataset, select_dataset, unload_dataset, snapshot_dataset, suppress_findings, unsuppress_findings, ingest_reports, normalise_findings, reachability_findings, map_inventory, collect_assets, run_scanners, run_pipeline. |
read_fs |
Reads files. | load_dataset, ingest_reports, run_scanners (for iac_dir). |
write_fs |
Writes files under the output directory. | export_ontology, export_terraform, export_report, run_scanners, run_pipeline. |
cloud_access |
Calls provider APIs with the server's credentials. | map_inventory, collect_assets, run_scanners, run_pipeline. |
exec |
Starts external processes. | run_scanners, run_pipeline. |
reveal |
Reverses pseudonymisation. | reveal_token. |
list_frameworks and the four meta tools declare no capability at all. A policy can cap sensitivity (max_sensitivity) and deny capabilities; with hide_denied: true (the default in the shipped profiles) a denied tool disappears from tools/list, and calling it anyway gives Unknown tool. Measured with layer.tools_wire() for the default local principal, each built-in profile hides these tools:
| Profile | Visible tools | Hidden |
|---|---|---|
open |
74 | none |
standard |
73 | reveal_token |
read_only |
66 | collect_assets, export_ontology, export_report, export_terraform, map_inventory, reveal_token, run_pipeline, run_scanners |
airgapped |
69 | collect_assets, map_inventory, reveal_token, run_pipeline, run_scanners |
audit |
66 | collect_assets, export_ontology, export_report, export_terraform, map_inventory, reveal_token, run_pipeline, run_scanners |
strict |
61 | collect_assets, export_ontology, export_report, export_terraform, get_asset_metadata, map_inventory, preview_transform, privacy_audit_log, reveal_token, run_pipeline, run_scanners, sparql_query, subgraph_export |
soc-analyst |
62 | collect_assets, export_ontology, export_report, export_terraform, get_asset_metadata, map_inventory, privacy_audit_log, reveal_token, run_pipeline, run_scanners, sparql_query, subgraph_export |
Under soc-analyst the analyst role sees 60 tools (it also loses rate_limit_status and terraform_preview) and the lead role 70 (only the four live tools are hidden).
The CLI flag --read-only is different from the read_only profile: it removes tools with write_fs, cloud_access or exec and every tool annotated destructive, so it also drops unload_dataset.
Annotations¶
Every tool sets the four MCP behaviour hints. When a tool leaves one unset, ToolSpec._derive_hints fills it from the capabilities: readOnlyHint is false when the tool needs write_state, write_fs or exec; destructiveHint defaults to false; openWorldHint is true only with cloud_access; read-only tools default to idempotent. The per-tool lines below show the final values. Only unload_dataset is destructive, and only map_inventory, collect_assets, run_scanners and run_pipeline are open-world. The tools that change state but are not read-only and not idempotent are load_dataset, snapshot_dataset, ingest_reports, reachability_findings and the four collecting live tools.
On the wire the annotations and cloudg's own metadata look like this (from find_assets):
{
"title": "Find assets",
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
}
{
"cloudg/category": "inventory",
"cloudg/sensitivity": "confidential",
"cloudg/tags": [
"start-here"
]
}
cloudg/tags marks the four tools tagged start-here (workspace_status, find_assets, list_findings, list_capabilities).
Output schemas¶
18 tools publish an outputSchema: workspace_status, diff_datasets, dataset_summary, find_assets, get_asset, count_assets, neighbors, get_edges, find_paths, attack_paths, lateral_movement_paths, depends_on, dependents, list_findings, findings_for_asset, top_risks, compliance_summary and sparql_query. Every property in these schemas is optional and every object allows extra keys, so projection and redaction never make a result invalid. The layer still withholds outputSchema from tools/list whenever the caller's output pipeline is not empty, because masking can change value types; under standard no tool advertises one, under open all 18 do.
Resource links¶
Tools attach resource_link content blocks pointing at the resources that hold more detail. A link has uri, name, an optional title, mimeType and sometimes annotations.priority (1.0 on the dataset summary link of the live tools). Which tool links what is listed in each entry. Links go through the same output transforms as the result, so a pseudonymised name stays pseudonymised in the link.
Errors¶
There are two kinds of failure.
A tool error comes back as a normal result with isError: true, the message in content[0].text and the details in _meta: cloudg/error_code and, when there is structured detail, cloudg/error_data. The model can read it and retry. The message never repeats the value the caller passed and never lists names from the dataset: the value, close matches and valid choices are in cloudg/error_data (value, suggestions, valid, close_matches), which goes through the caller's output pipeline like a result, so a pseudonymising policy pseudonymises them. Bad arguments, unknown datasets, unresolved references, path violations, denied calls, timeouts and handler exceptions all take this route. Argument validation happens before the handler runs and reports pydantic's error list:
{
"content": [
{
"type": "text",
"text": "Invalid arguments: limit: Input should be a valid integer, unable to parse string as an integer"
}
],
"isError": true,
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": [
{
"loc": "limit",
"msg": "Input should be a valid integer, unable to parse string as an integer",
"type": "int_parsing"
}
]
}
}
A protocol error is raised instead of returned. call_tool raises NotFoundError (JSON-RPC -32602) for a tool that does not exist or that the policy hides:
Resource reads and prompt requests raise for every failure (unknown URI, unknown asset, missing prompt argument), and adapters turn that into a JSON-RPC error.
cloudg/error_code |
Raised for |
|---|---|
-32602 |
Invalid arguments, unknown dataset, asset, finding, control, enum value or cursor, a dataset name that is already taken. NotFoundError and InvalidArgumentsError share this code. |
-31001 |
Access denied: a path outside the allowed roots, an output path escaping the output directory, a policy denial, or force=true on a live tool without the admin or operator role. |
-31029 |
Rate limited: a policy rate limit, or the live operation guard refusing a collection (cooldown, busy, caller quota; see Live operation guard). |
-32603 |
Generic layer error, used by the live tools' credential preflight. |
"timeout" |
The tool ran past its timeout (300 s by default, longer where the entry says so). |
"handler_error" |
An unexpected exception in a tool handler; the text is ExceptionType: message, passed through the output pipeline. Resource and prompt handlers never send raw exception text. |
Tools never accept arguments outside their schema (additionalProperties: false):
{
"tool": "find_assets",
"arguments": {
"name": "web-1"
},
"isError": true,
"text": "Invalid arguments: name: Extra inputs are not permitted",
"_meta": {
"cloudg/error_code": -32602,
"cloudg/error_data": [
{
"loc": "name",
"msg": "Extra inputs are not permitted",
"type": "extra_forbidden"
}
]
}
}
Progress and change notifications¶
Two kinds of tool report progress when the client sends a progress token. ontology_stats, sparql_query, ontology_neighbourhood, relation_groups and export_ontology send 0/1 building ontology for N assets and 1/1 ontology built the first time a dataset's ontology is built; later calls use the cache and send nothing. The live tools report one notification per pipeline phase and a final done (see Live). Progress values only increase; the context drops any notification that does not.
The workspace also tells the layer when data changes. Loading or unloading a dataset sends notifications/resources/list_changed; any change also sends notifications/resources/updated for cloudg://workspace, cloudg://datasets and cloudg://datasets/{name}/summary. Selecting, snapshotting, suppressing, ingesting, normalising and the live tools all trigger these.