/ Docs Guides / MCP server server.json ← All guides
Agents

The MCP server — 148 tools, your credential, your tier.

The IG1 MCP server (ig1-mcp 0.9.4, MCP SDK 2.0.0, spec 2026-07-28 stateless) exposes cloud operations as Model Context Protocol tools over streamable HTTP at https://mcp.cloud.ig1.com/mcp. It holds no identity of its own: every request carries the caller's Bearer token, which is forwarded to the backing service on every tool call — the IG1 API resolves project + tier server-side, exactly as if you called REST directly. The tool count in the title is the size of the server's tool registry; the catalog below lists every one.

Auth model

Tool catalog

Each tool with its minimum tier (0 read, 1 operate, 2 destructive). The tier noted is the effective one — the API gates by HTTP method, so reading a console log (a POST in Nova) is tier 1 even though it feels like a read. The one enumerated exception: detaching a volume (an HTTP DELETE upstream) is tier 1, because a detach is reversible and data-preserving — symmetric with attach.

Compute

ToolTierPurpose
list_vms0list virtual machines for the caller's project
create_vm1create a VM (name, flavor, image, network)
vm_action1power actions: start | stop | reboot (SOFT)
resize_vm1resize to a new flavor, then confirm or revert
confirm_resize1commit a pending resize
revert_resize1roll a pending resize back to the prior flavor
get_console_log1read the serial console log (a POST in Nova → tier 1)
snapshot_vm1snapshot a VM to a bootable Glance image — crash-consistent; stop first if the workload cannot tolerate that
shelve_vm1power off AND free the host resources (disk snapshotted to Glance; reversible)
unshelve_vm1bring a shelved VM back
delete_vm2delete a VM

Keypairs

ToolTierPurpose
list_keypairs0list SSH keypairs
import_keypair1import a public key — private-key generation is not exposed (see exclusions)
delete_keypair2delete a keypair

Volumes & snapshots

ToolTierPurpose
list_volumes0list block-storage volumes
create_volume1create a volume (name, size in GB)
attach_volume1attach a volume to a server
detach_volume1detach a volume (a DELETE upstream, but an enumerated tier-1 exception: reversible and data-preserving)
extend_volume1grow a volume — Cinder never shrinks; the guest still grows the filesystem itself
delete_volume2delete a volume
list_volume_snapshots0list volume snapshots
create_volume_snapshot1snapshot a volume
delete_volume_snapshot2delete a volume snapshot
create_volume_from_snapshot1new volume from a snapshot
list_volume_backups0list volume backups — the OFF-cluster copies (platform object store, phase 58)
create_volume_backup1back up a volume off the block layer — the DR copy a snapshot is not
delete_volume_backup2delete a volume backup
restore_volume_backup1restore a backup into a NEW volume — never over the source

Network

ToolTierPurpose
list_networks0list Neutron networks
create_network1create a network
delete_network2delete a network
create_subnet1create a subnet — the CIDR is validated strictly before Neutron sees it (Neutron silently "fixes" a typo'd host octet)
delete_subnet2delete a subnet
list_routers0list routers
create_router1create a router — no gateway parameter exists (see exclusions)
add_router_interface1attach a subnet to a router
remove_router_interface1detach a subnet (a PUT in Neutron — the reversible half)
delete_router2delete a router — refuses a gateway-bearing one rather than clearing the gateway first
list_ports0list ports
list_floating_ips0list floating IPs
allocate_floating_ip1allocate a FIP (default: first router:external network)
associate_floating_ip1attach a FIP to a server
disassociate_floating_ip1detach a FIP, keep the address (deliberately not tier 2: the reversible half)
release_floating_ip2release a FIP back to the pool (a DELETE at the API → tier 2)
list_nat_gateways0list this project's NAT gateways (0 or 1). The platform provides outbound internet access (SNAT) for every tenant router — one per tenant by design
create_nat_gateway1create or converge a NAT gateway for this project. Idempotent: if a gateway already exists, the existing record is returned
delete_nat_gateway2delete a NAT gateway — disables SNAT on the tenant's router. The router's external gateway network survives so floating IPs continue to work

Security groups

ToolTierPurpose
list_security_groups0list security groups
create_security_group1create a group — default-deny: with just a name you get an empty group
list_security_group_rules0list a group's rules
add_security_group_rule1add one rule — the remote is never implicit, same posture as create
remove_security_group_rule2remove a rule — revoking access can cut a running workload off (a DELETE → tier 2)
delete_security_group2delete a group
Default-deny is deliberate, and different from what you may expect. A newly created group has no rules — it used to seed ICMP + SSH/22 from 0.0.0.0/0, which meant "make me a VM" published SSH to the internet as a side effect. ingress_cidr re-adds the convenience pair (ICMP + SSH/22) from that range only; extra_rules requires remote_ip_prefix on every rule; and any rule opening TCP/22 to the whole internet is refused unless allow_ssh_from_anywhere=true — before the group is created, so nothing half-open is left behind.

Identity

ToolTierPurpose
whoami0who this token is — person, role, account (name, alias, number), project and tier. Call it first: every other tool acts on « the caller's project », and the same token is a sandbox in one context and a production account in another. A cross-tenant credential says so in capitals
get_effective_org_policy0the organization guardrails in force for this credential, and which policy produced each one. Call it when a write is refused and the tier looks right: an account can attach deny-only policies to itself or to an OU, they inherit downwards and compose by minimum, and they can lower the tier a credential effectively holds. Read-only by design — an agent that could delete a policy could lift its own ceiling
list_org_units0the account's organizational units and which projects sit in each. The path is the parentage (prod.eu-west is a child of prod), and two projects in different OUs can be subject to entirely different ceilings
list_projects0the projects this account holds and how many more its tier allows. A project is a real isolation boundary — its own quota, network, object-storage namespace and Kubernetes namespace — so two of them may each hold a cluster called prod. Marks the one in effect, and whether each is addressable by this credential (an account can own a project a given token cannot reach)
create_project2add another isolated project to this account, provisioned at the account's tier with its own quota, network and OpenStack credential. Billed capacity, refused at the tier's ceiling, and reserved to the Owner/Administrator rungs. There is deliberately no delete verb — removing a project destroys everything in it and stays an operator action
list_workload_identities0list this project's workload identity configurations. One config per cluster; shows which clusters can exchange Kubernetes service-account tokens for IG1 credentials
create_workload_identity1enable workload identity (IRSA) for a Kubernetes cluster. Registers the cluster's OIDC issuer with the IG1 identity plane so that pod service-account tokens can be exchanged for short-lived IG1 credentials
delete_workload_identity2delete a workload identity configuration. Pods in the cluster will no longer be able to exchange service-account tokens for IG1 credentials
exchange_workload_token1exchange a Kubernetes service-account JWT for an IG1 credential. The JWT must be signed by a cluster whose OIDC issuer is registered with IG1. The returned credential is scoped to the tenant's project and expires in 1 hour

Images & flavors

ToolTierPurpose
list_images0list Glance images visible to the project
get_image0one image's detail
import_image1web-download import — SSRF-guarded: private/link-local/loopback targets are refused before any image record is created
delete_image2delete an image
list_flavors0list Nova flavors (VM sizes)
get_quotas0the project's quota tier, limits and current use — check before creating; nothing on this surface changes a tier
get_tiers0the tier catalogue — every tier's name and headline ceilings (the menu; what the project holds is get_quotas). Never the platform's capacity internals, and the change itself stays an operator decision

Kubernetes (via the KaaS factory)

ToolTierPurpose
list_clusters0list tenant clusters (phase, version, workers, endpoint)
get_kubernetes_cluster0one cluster's detail, incl. its LoadBalancer services with addresses (phase 53)
create_cluster1provision a cluster (async — poll list_clusters for ready)
scale_cluster1scale the worker plane — refused (409) while autoscaling is enabled: one desired count, one writer
set_cluster_autoscaling1hand the worker count to the cluster-autoscaler within [min, max], or take manual control back — max is what quota admission charges
upgrade_cluster1rolling upgrade — Kubernetes has no downgrade, so this is write-ahead audited despite being tier 1
set_cluster_protection1set the deletion-protection flag
list_cluster_versions0available upgrade targets
delete_cluster2deprovision a cluster
get_cluster_kubeconfig0the admin kubeconfig — sensitive: returned to the caller only, never logged or audited

VM autoscaling groups (via the KaaS factory)

ToolTierPurpose
list_asgs0list autoscaling groups — current next to desired, so drift is visible at a glance
create_asg1create a group (flavor, image, min/max/desired, optional tcp health check, LB pool binding, alarm bindings, cooldown) — quota admission runs before anything is stored
get_asg0full detail: members with their drain/health standing, the last scaling decision, and degraded — every reason the engine could not act
update_asg1move min/max/desired manually — manual beats alarm, and a changed desired starts the cooldown clock
delete_asg2tear down the group and every member VM — drain-first: pool removal, drain window, then deletion
Groups scale on alarm transitions, never on levels. Only an ok → alarm transition of a bound alarm moves capacity by one; insufficient_data never scales in either direction, a stale alarm state reads as insufficient_data, and both directions firing at once resolves to scale-out (availability over cost). These tools ride the factory service (/v1/asgs), like the cluster tools — not the API gateway.

Object storage

ToolTierPurpose
get_project_contents0what deleting this project would destroy. The counterpart to the delete verb that is deliberately absent — an agent can inventory a project so a human decides with the list in front of them
list_buckets0list S3 buckets
create_bucket1create a bucket
list_bucket_objects0one page of a bucket's keys (S3's own continuation-token paging). Keys, never object bytes
presign_bucket_object0a temporary download link for one object — treat the result as a credential with a short life, not a path: anyone it is passed to can fetch that object until it expires
get_bucket_versioning0is versioning on? Worth checking before any bulk delete: on a versioned bucket a delete writes a marker and keeps — and bills for — every version
set_bucket_versioning1turn versioning on, or suspend it. S3 offers no way back to "never versioned": suspended stops keeping NEW versions and leaves every stored version in place, still billed
delete_bucket2delete a bucket
get_s3_credentials0the project's RGW access/secret pair — a live secret enters the transcript; reading does not rotate (see guardrails)
rotate_s3_credentials1issue a new S3 keypair — RGW rotation adds; the old pair stays valid until retired separately

Databases

ToolTierPurpose
list_databases0list managed databases
create_database1provision a managed database (async — poll get_database for ready)
get_database0one database's detail and status
resize_database1grow storage — grow-only; storage never shrinks
restore_database1restore a backup-enabled instance into a NEW database — never in place (phase 58)
get_database_credentials0the connection secret — a live secret enters the transcript; reading does not rotate (see guardrails)
delete_database2delete a database

DNS (Designate)

ToolTierPurpose
list_dns_zones0list DNS zones
get_dns_zone0one zone's name, TTL, status and SOA serial — the serial is how you tell whether an edit has reached the servers that answer for it
create_dns_zone1create a zone for a domain you control — refused inside IG1's own namespace, and above the tenant's zone ceiling
delete_dns_zone2delete a zone AND every record in it — the whole namespace stops resolving; write-ahead audited
get_dns_delegation0is the domain actually delegated here? Creating a zone is half the job — the registrar still has to point the NS records at us, and until it does the zone holds records nothing on the internet asks for. The tool to reach for when "I created the record and it still does not resolve": usually the record is fine and the delegation was never published
import_dns_records1create many recordsets from a zone file. Previews by default — the plan a dry run returns is the same shape the commit returns, the same call rather than a second code path that can disagree. Re-running a commit is safe: an entry already present with the same values comes back identical, not as an error
list_dns_records0list a zone's recordsets
create_dns_record1create a recordset — a short name is qualified against its zone by the gateway
update_dns_record1change what a record points at, in place — delete+create would take the name out of resolution in between
delete_dns_record2delete a recordset — blackholes a name other people's clients are resolving; write-ahead audited

Designate may not be deployed in a given environment; these tools then forward the gateway's 404 verbatim rather than reporting an empty zone list — "no zones" and "no DNS service" are different facts.

Load balancing (Octavia)

ToolTierPurpose
list_load_balancers0list load balancers, each with its public facet — hostname + pending/active when internet-facing, internal only otherwise
get_load_balancer0one LB's detail including member_health — every member's operating status and the pool's monitor id, so a dead member is found by reading, not by bisecting backends — and the same public facet
create_load_balancer1one composite call builds VIP + listener + pool + health monitor + members, or rolls back what it made, newest first; internet_facing=true (default false) then publishes the VIP through the .75 edge — a floating IP on the VIP port, then an exposure targeting it — and answers with the hostname and its honest pending/active status; manage_security_group (default true) is what makes the listener answer at all — see the callout below
delete_load_balancer2cascade delete — every live connection through the VIP drops, and a replacement gets a different address; an internet-facing LB's exposure and floating IP are released first (exposure → FIP → LB), and if they cannot be found the answer says they may remain; the managed listener group goes last, once the cascade has released the VIP port
A load balancer used to be born unreachable, and every status said it was fine. Octavia's OVN provider gives the VIP port the tenant's default security group, which denies all ingress: the load balancer read ACTIVE, its members read ONLINE, and the listener answered nothing — the worst failure shape an agent can hand a human, because there is no field to notice. So create_load_balancer now also creates the group {name}-lb, opens each distinct listener port on it (protocol following the listener), and attaches it to the VIP port beside whatever is already there — the port is read first, never clobbered — and reports security_group_id on create, get and list. 0.0.0.0/0 is the honest default: the boundary is the network, not this rule (the VIP is reachable only where the tenant already permits — east-west, or through a floating IP or edge exposure someone asked for), and a narrower default would recreate this exact bug the moment internet_facing=true is passed. Narrow the group afterwards with the security-group tools if the workload deserves it. manage_security_group=false opts out — nothing created, nothing attached — and the answer then says the VIP refuses traffic on the listener port until a group allowing it is attached; it is never silent about that. delete_load_balancer deletes only a group this platform created: named exactly {name}-lb and attached to that VIP port. A group a human attached is never touched, and a group that could not be deleted is named as possibly remaining. If the whole hop fails, the load balancer is kept and the answer says the listener is not reachable yet — it never claims a working listener it did not open.
The health monitor is on by default — the AWS posture, where a target group without health checks does not exist. Without one, OVN keeps hashing flows onto a dead member forever. The default probe follows the listener protocol (TCP → TCP, UDP → UDP-CONNECT, the only two the OVN driver implements), every 5 s with a 3 s timeout; 3 consecutive failures mark a member ERROR and stop new flows to it. health_check=false opts out, with the consequence stated in the answer. Two things to design around: L4 only (TCP/UDP passed through — no TLS termination at the LB, no L7 rules; put the certificate on the backends) and internal by default (the VIP lives on a tenant subnet and creates no north-south exposure, so an unpublished one that is unreachable from the office is behaving as designed). internet_facing=true is the opt-in: the tool composes a floating IP on the VIP port and an edge exposure targeting it, and answers with {name}-{tenant-slug}.10.57.8.75.nip.io and its status — pending until the edge syncs, never asserted active. TCP listeners only, and the members must serve TLS on the member port — the edge re-encrypts to the VIP, so a plain-HTTP backend fails the handshake and leaves a hostname that resolves and never answers (a self-signed certificate is fine; the edge does not verify it). A UDP listener plus internet_facing is refused with the reason before the first call. If publishing fails the load balancer is kept and the answer names what exists and how to finish by hand — it never claims internet-facing on a floating IP alone. The certificate on the generated hostname is the internal-CA wildcard; a customer domain attached to the exposure gets a public one (see the edge callout below).

Edge exposures (the .75 tenant ingress)

ToolTierPurpose
list_edge_exposures0the caller's published hostnames with derived status — pending until the edge acknowledges the config, never a green light nothing has wired
create_edge_exposure1publish {name}-{tenant-slug}.10.57.8.75.nip.io → an IP the project owns — ownership verified server-side against Neutron under the caller's own credential, fail-closed
get_edge_exposure0one exposure with its derived status and domain state — follow a single change instead of re-listing every exposure
delete_edge_exposure2un-publish — takes a public hostname down on the edge's next sync; write-ahead audited
claim_edge_domain1claim a domain you own (app.example.com) on an exposure — reserved, not routed; the answer carries the exact _ig1-challenge TXT record to publish. Claims are unique across the whole platform, first claim wins
verify_edge_domain1read that TXT record back — this is the step that starts routing. Idempotent; 400 means the record is not published yet (it shows what was found), 503 means our resolver could not answer and says so
release_edge_domain2stop serving the custom domain — the exposure and its .nip.io hostname survive, but the name returns to the global pool and another tenant may claim it; write-ahead audited
This is the north-south half of the LBaaS story above. Two doors exist since 2026-08-26: a floating IP from the public pool (185.255.84.64/26) attached to the workload — direct L4, any TCP — and the edge gateway at 10.57.8.75, the name-based HTTPS door that costs zero public IPv4, used directly with create_edge_exposure, or on your behalf by create_load_balancer(internet_facing=true), which is these tools composed and cleaned up by delete_load_balancer. Read "internet-facing" with its bound: it publishes the VIP through the platform edge at 10.57.8.75 under a generated .nip.io hostname, and that name resolves to a private address — fabric/VPN reach, not the internet. TLS terminates at the edge with an internal-CA wildcard certificate for *.10.57.8.75.nip.io; clients install the lab CA, exactly as they already do for the portal, and that will not change, because a public CA cannot validate a name that resolves into private space. The public path is a domain the customer owns, attached with claim_edge_domain + verify_edge_domain: since 2026-08-25 the edge answers on 185.255.84.178, cloud.ig1.com resolves worldwide, and a verified custom domain is served with a publicly-trusted Let's Encrypt certificate ordered automatically (networking guide §6.1). Targets must belong to the caller's project: a foreign or fabric address is refused with the reason, and an unverifiable one is a 503, never a grant.

Observability

ToolTierPurpose
list_events0the project's own event stream — provisioning, quota, billing and agent.actions rows
list_event_topics0the topics a webhook can subscribe to. Without it an agent creating a webhook had to guess the names — and a subscription to a topic that does not exist is accepted and then silently never fires
get_audit_log0the audit trail: who did what, when, through which surface — including this agent's own calls; nothing on this surface writes or prunes it
get_instance_metrics0CPU / memory / disk / network for one instance — the hypervisor's view, not the guest's
get_instance_metrics_history0CPU / memory history over a window — what its load looked like at 03:00, not just now; Prometheus-backed, so history ends at retention
get_status0platform component health and open incidents — check first when several unrelated things fail at once
list_webhooks0list event webhooks
create_webhook1subscribe an HTTPS endpoint to the project's event stream — the subscription outlives the session (see guardrails)
list_webhook_deliveries0recent delivery attempts and their outcomes — the diagnostic for "my webhook is not firing", because it separates the three cases that look identical from outside: nothing sent, sent and refused, or sent and accepted with the problem downstream
delete_webhook2remove a webhook — deliveries stop immediately

Billing & budgets

ToolTierPurpose
get_usage0rated usage (EUR) for the project over a billing period
get_cost_breakdown0costs broken down by service/resource
get_cost_forecast0projected spend for the period
list_invoices0list invoices
list_budgets0list budgets
create_budget1create a spend budget
delete_budget2remove a spend guardrail — a silent change nobody notices until the invoice, so it is write-ahead audited

Secrets (Barbican)

ToolTierPurpose
list_secrets0the project's stored secrets — metadata only: names, ids, status, type, expiry; never a value
get_secret_metadata0one secret's metadata — never the payload (see the callout)
create_secret1store a secret — write-only from here: the caller supplies the value (it already holds it), the tool returns the id and never echoes the value
delete_secret2delete a secret, payload included — no undo, and (because this surface cannot read payloads) no copy an agent could have kept; write-ahead audited
Secret payload read is not a tool, by decision. A payload read by a tool lands in the model's context, the conversation transcript and the runtime's logs — none of which can be revoked, all of which outlive every later rotation of the secret itself. Consumers fetch the value at use time with their own credential; a human who needs to see one uses the portal or CLI reveal-once flow. The asymmetry is the point: an agent can provision, wire up, rotate (create-new → repoint → delete-old) and clean up secrets end to end without ever becoming a place a value leaks from later.

IAM

ToolTierPurpose
list_credentials0list the project's API credentials (never their secrets)
get_credentials_usage0when each credential was last used. list_credentials says what exists; this says what anything is still using, which is what makes a clean-up safe — the untouched one is safe to revoke, an old one in daily use is not
create_credential1mint a scoped credential — tier ≤ 1, expiry mandatory and ≤ 7 days, defaults tier 0 / 7 days (see guardrails)
revoke_credential2instant and total: kills the PAT, the client secret and every token already minted from it

Workflow

ToolTierPurpose
run_workflow1an ordered step list from a fixed op allowlist, one audited result — tier 1 to create, tier 2 required to enable rollback

run_workflow — the sandbox

Steps come from a fixed op allowlist: create_vm, create_volume, attach_volume, allocate_floating_ip, associate_floating_ip — no free-form code, no shell. A step may reference a previous step's resource as $step<N>.id (1-based), so "create a VM, create a volume, attach it, allocate an address, attach the address" is one call with one aggregated, audited result. The associate step exists because the canonical workflow without it ends with an address attached to nothing — allocated, billed, status DOWN.

The rollback pre-flight. On failure the workflow rolls back what it created, in reverse order — but rollback issues DELETEs, which the API gates at tier 2. So a workflow with rollback=true (the default) refuses to start unless the caller's credential is tier ≥ 2. A tier-1 caller used to get halfway, fail, and have every rollback DELETE 403 — leaving real VMs, volumes and floating IPs orphaned, billed, against quota. Either bring a tier-2 credential, or pass rollback=false to run anyway and own the cleanup deliberately — a failed run's result names every resource that survived.

Guardrailed, not banned

Four capabilities on this surface create or expose things that outlive the session — a minted credential has no parent in the store, so revoking the token that made it does not cascade. They used to be absent; the operator decided the surface needs them, and the argument against them was not wrong, so it is encoded as limits. The limits are pinned by tests, not by convention:

ToolThe limit, and why
create_credentialTier 2 is not mintable — destructive authority stays a human decision. Expiry is mandatory, capped at 7 days (tightened from 30 on 2026-08-13): the API accepts a no-expiry credential; this surface does not, because "forever" is what makes persistence permanent, and a week is long enough for any task an agent is plausibly running yet short enough that a forgotten key dies before anyone would have noticed it to revoke it. Defaults are tier 0 / 7 days: a caller that does not say what it needs gets the least that could work. The gateway independently enforces "cannot outrank creator".
create_webhookHTTPS-only, here as well as at the gateway — and the tool says plainly that the subscription outlives the session, the conversation and the token, and that a URL which came from anywhere but the operator is a reason to stop and ask.
get_s3_credentials, get_database_credentialsEach returns a live, long-lived secret into model context and the transcript. Reading does not rotate it — the value stays valid wherever a copy has come to rest — and the tools say so and name the rotate path.
rotate_s3_credentialsSays the quiet part: RGW rotation adds a keypair, it does not invalidate the old one. Retiring the previous pair is a separate deliberate act.

The writes among these (create_credential, create_webhook) sit in the destructive-audit set: they are write-ahead audited and refuse to act when the caller cannot be attributed — an unattributable credential mint is precisely the event the trail exists for. The audit records parameter names only, so a minted secret or PAT never reaches the bus.

Excluded by decision

What an agent cannot do through this server, on purpose. None of these are gaps — each is a decision, and each has one reason. If your integration needs one of them, that is what the portal, the CLI and a human are for.

CapabilityWhy it is not a tool
Database password rotationRotation breaks every application still holding the old password the moment it reconnects — a disruptive, timing-sensitive act on somebody else's running workload, with no undo. The portal and the CLI carry it, behind a typed confirm that spells out the reconnect behaviour.
Tenant tiers & onboarding (set_tenant_tier, create_tenant, approve_signup, org-user creation)Re-tiering and onboarding spend finite platform capacity across every tenant — an operator act. get_quotas reads; nothing on this surface writes.
Router external gatewaycreate_router has no gateway parameter at all — refusal by schema, because a parameter that does not exist cannot be argued into existence by a persuasive prompt. External addresses are platform-governed: your tenant's one uplinked router is provisioned by IG1 (before 2026-08-15 a second gateway-bearing router blackholed every tenant's data plane — two real outages taught this; the substrate is fixed since, gotcha 151, and every tenant router carries its own gateway). delete_router likewise refuses a gateway-bearing router rather than clearing the gateway first — that router is your uplink.
Private-key generationimport_keypair accepts the public half only; Nova's server-side key generation is unreachable. A private key generated into model context lives in the transcript forever.
Secret payload readBarbican serves GET …/secrets/{id}/payload and no tool here calls it — a value read into model context outlives every revocation, exactly the reasoning that once kept S3 keys and database passwords off this surface. A stored secret differs from those in one decisive way: its consumers fetch it themselves at deploy time, so an agent can do its whole job — create, wire up, rotate, delete — without ever holding the value. Humans use the portal/CLI reveal-once flow. Writing a payload in (create_secret) is fine: the caller already holds it, and the tool never echoes it back.
Tier-2 or non-expiring credentialscreate_credential cannot mint destructive authority and cannot mint "forever" — the two properties that turn one leaked token into a durable fleet of them.

Agent quickstart

Five minutes from zero to your agent listing your VMs:

# 1. mint an agent credential (secret prints ONCE) — start read-only
ig1 credential create --kind mcp --tier 0 --label claude-laptop

# 2. print the MCP client JSON for this context
ig1 mcp config

# 3. install the IG1 skill + AGENTS.md into your project
ig1 agent init

The ig1 mcp config output drops straight into your runtime:

{
  "mcpServers": {
    "ig1": {
      "type": "http",
      "url": "https://mcp.cloud.ig1.com/mcp",
      "headers": { "Authorization": "Bearer ${IG1_TOKEN}" }
    }
  },
  "ig1": {
    "issuer": "https://zitadel.cloud.ig1.com",
    "token_endpoint": "https://zitadel.cloud.ig1.com/oauth/v2/token",
    "scope": "openid urn:zitadel:iam:org:project:roles urn:zitadel:iam:org:project:id:385253743135817916:aud",
    "cacert": "ig1-internal-ca.pem",
    "client_id": "<from ig1 credential list>"
  }
}

ig1 agent init writes AGENTS.md (read-only-first norms, tiers, error semantics) and the full skill to .tidet/skills/ig1/SKILL.md and .claude/skills/ig1/SKILL.md — the same documents that live in-repo at docs/agent/ (ig1-skill/SKILL.md + AGENTS.md), which remain the source of truth.

Audit & error semantics

Every tool call emits an agent.actions audit event — tool name, parameter names (never values), started/ok/error — carrying the real actor and project resolved from the caller's token, visible in the portal (Security → activity) and through get_audit_log. For read/write tools the audit is best-effort: a bus outage never fails the call. For destructive tools it is write-ahead: the event is recorded before the action, and the tool refuses to act if it cannot be — an unaudited delete is worse than a failed one. Membership tracks disruption, not the HTTP verb: upgrade_cluster (a tier-1 POST with no downgrade) and delete_budget (a silent guardrail removal) are both in. Kubeconfig content, tokens and secrets are never logged. What to do with failures:

ResultMeaning → action
401token/credential dead (expired or revoked) → re-issue the credential; stop, do not retry in a loop
403 insufficient credential tiertier too low for the action → report the required tier; never retry
403 no tenant assignedthe credential's store entry is gone → re-issue in the Security page
404 on an idunknown OR cross-tenant — existence is never leaked by design
404 from the DNS toolsDesignate is not deployed in this environment — forwarded verbatim, never softened into "no zones"
502 from the load-balancing toolsOctavia is not answering (it may not be deployed) — a 404 from these paths is Octavia itself saying the id is not this tenant's
429rate limited → back off per Retry-After, then retry once