# Cloud

> Step-by-step connection and configuration for AWS, AWS CloudWatch, AWS SQS / SNS, Azure, Azure Monitor, Google Cloud Logging, Cloudflare, Vercel and Okta.

Source: https://triagic.com/docs/integrations/cloud

The platform layer: logs, metrics, queues and deployments belonging to the cloud
provider rather than to your application.

## AWS CloudWatch [#aws-cloudwatch]

`aws-cloudwatch` · runs `awslabs.cloudwatch-mcp-server` via **uvx**.

Overview, prompts and the tool list: [/integrations/aws-cloudwatch](/integrations/aws-cloudwatch).

1. **Create an IAM user** with `CloudWatchReadOnlyAccess` (and
   `CloudWatchLogsReadOnlyAccess` if you want log queries), then generate an access key.

2. **Or use a named profile instead, if long-lived keys are not what you want.**

   Choosing *Named profile* reads the credential out of the `~/.aws/config` of the machine
   running Triagic: that machine's file, not the portal host's. It is the only route to
   assume-role, IAM Identity Center (SSO), `credential_process` and MFA, because none of
   these servers exposes a role ARN or an SSO setting of its own.

   A profile that assumes a role looks like this:

   ```ini
   [profile triagic-readonly]
   role_arn = arn:aws:iam::123456789012:role/triagic-readonly
   source_profile = default
   region = us-east-1
   ```

   and an IAM Identity Center profile like this, after `aws sso login --profile triagic-sso`:

   ```ini
   [profile triagic-sso]
   sso_session = corp
   sso_account_id = 123456789012
   sso_role_name = ReadOnly
   region = us-east-1
   ```

   With a profile selected, no access key is sent at all. That matters: the AWS SDK treats
   an empty access key as a real credential, finds it first, and never reads the profile,
   which is why the key fields disappear rather than being left blank.

3. **Fill the form.** All AWS integrations share this credential shape.

   | Field                     | Required                     | What to put                                                                                                           |
   | ------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
   | **Authentication method** | no, defaults **Access keys** | *Named profile* for assume-role, SSO, `credential_process` or MFA.                                                    |
   | **AWS access key ID**     | yes, with *Access keys*      | `AKIA…`                                                                                                               |
   | **AWS secret access key** | yes, with *Access keys*      |                                                                                                                       |
   | **Session token**         | no, with *Access keys*       | Only for temporary (STS) credentials.                                                                                 |
   | **Profile name**          | yes, with *Named profile*    | The name inside `[profile …]`: `triagic-readonly` above, not `profile triagic-readonly`.                              |
   | **Region**                | yes, defaults `us-east-1`    | Log groups and alarms are per-region. Add a second instance for a second region.                                      |
   | **CA certificate path**   | no                           | Only where a TLS-intercepting proxy sits in front of the AWS endpoints. Absolute path on the machine running Triagic. |

4. **Verify.** Start-checked only, no health check runs. The first tool call is what
   proves the credential.

| If it reports                                        | It usually means                                                                                                                                             |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `The config profile (…) could not be found`          | No such profile in the `~/.aws/config` of the machine running Triagic. Check the name inside the brackets, and remember that machine is not the portal host. |
| `Token has expired and refresh failed`               | An SSO profile whose session has lapsed. Run `aws sso login --profile …` on that machine.                                                                    |
| `InvalidClientTokenId`, `SignatureDoesNotMatch`      | With access keys: the ID and secret are not a matching pair, or the key is inactive.                                                                         |
| `CERTIFICATE_VERIFY_FAILED` reaching an AWS endpoint | A TLS-intercepting proxy. Set **CA certificate path** to its CA bundle.                                                                                      |

## AWS SQS / SNS [#aws-sqs--sns]

`aws-sqs-sns` · runs `awslabs.amazon-sns-sqs-mcp-server` via **uvx** · same credential
fields as CloudWatch above.

Overview, prompts and the tool list: [/integrations/aws-sqs-sns](/integrations/aws-sqs-sns).

1. **Create an IAM user** with `AmazonSQSReadOnlyAccess` and `AmazonSNSReadOnlyAccess`, or
   a `~/.aws/config` profile as described under
   [AWS CloudWatch](#aws-cloudwatch) above.

2. **Fill the form**: authentication method, then either the access key pair or a profile
   name, plus the region and an optional CA path, exactly as above. The region must be the
   one the queues and topics live in.

3. **Verify.** Start-checked only.

## Azure Monitor [#azure-monitor]

`azure-monitor` · runs the official `@azure/mcp` server via **npx**, narrowed to the
`monitor` namespace and started with its `--read-only` flag.

Overview, prompts and the tool list: [/integrations/azure-monitor](/integrations/azure-monitor).

Covers Log Analytics and Application Insights KQL queries, the activity log, table
metadata, metric definitions and metric queries.

Whichever method you pick, the credential chain is pinned to it. Left unpinned, the
Azure SDK walks a chain that ends in a browser and a device-code prompt, so a bad
service principal hangs for five minutes instead of failing, and a stale `az login`
sitting on the machine can quietly satisfy the chain while the principal you
configured is broken.

1. **Register an app** in Entra ID → **App registrations → New registration**. Note its
   **Directory (tenant) ID** and **Application (client) ID**. Skip this step if you are
   using the Azure CLI method.

2. **Create a credential for it: a secret or a certificate.**

   Under **Certificates & secrets**, either **New client secret** or upload a
   certificate. A secret is quicker; a certificate lasts far longer. Client secrets cap
   out at two years and expire silently, and the resulting failure looks like a bad
   credential rather than an expiry, so a certificate is the better choice for an
   integration nobody is watching.

   For a certificate, save the file on the machine running Triagic and give the absolute
   path. PEM and PFX both work. A PEM must contain the private key as well as the
   certificate, and only a password-protected PFX needs the password field.

   For a local machine you already sign in on, **Azure CLI sign-in** stores no credential
   at all and uses whatever `az login` left behind. That sign-in has to belong to the
   user Triagic runs as, and the integration stops when it lapses.

3. **Grant the service principal two roles**: **Monitoring Reader** on the subscription,
   and **Log Analytics Reader** on the workspaces you want to query. Without these the
   sign-in succeeds and every query fails.

4. **Fill the form.**

   | Field                       | Required                             | What to put                                                                   |
   | --------------------------- | ------------------------------------ | ----------------------------------------------------------------------------- |
   | **Authentication method**   | no, defaults client secret           | Certificate for something long-lived, Azure CLI for a machine you sign in on. |
   | **Tenant ID**               | with either service-principal method | The app registration's Directory (tenant) ID.                                 |
   | **Client ID**               | with either service-principal method | The same registration's Application (client) ID.                              |
   | **Client secret**           | with secret auth                     |                                                                               |
   | **Client certificate path** | with certificate auth                | `/opt/triagic/azure-sp.pem`, the path on the machine running Triagic.         |
   | **Certificate password**    | no                                   | Only for a password-protected PFX.                                            |
   | **Subscription ID**         | yes                                  | The subscription to query.                                                    |

5. **Verify.** Health check lists Log Analytics workspaces, which fails on a bad
   credential *and* on a subscription the principal was never granted.

| If it reports                              | It usually means                                                                                                                                                     |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AADSTS7000215`, `invalid client secret`   | The secret expired, or the three ids don't all come from the same app registration. A certificate avoids the expiry cycle.                                           |
| `AADSTS700027`, `certificate is not valid` | Wrong path, unreadable by the user Triagic runs as, a PEM missing its private key, or a certificate that was never uploaded to the app registration.                 |
| `Please run 'az login'`                    | The CLI sign-in lapsed or was never made as the user Triagic runs as. Sign in again, or switch to a service principal so the integration carries its own credential. |
| `AuthorizationFailed` / `403`              | Signed in, but no role on the subscription. Grant Monitoring Reader, plus Log Analytics Reader on the workspaces.                                                    |
| `subscription not found`                   | Wrong id, or the principal lives in a different tenant.                                                                                                              |

## Google Cloud Logging [#google-cloud-logging]

`gcp-logging` · runs `@google-cloud/observability-mcp` via **npx** · every tool is
read-side, so there is no write mode to disable.

Overview, prompts and the tool list: [/integrations/gcp-logging](/integrations/gcp-logging).

Covers Cloud Logging entries, log names, buckets and sinks; Cloud Monitoring metric
descriptors, time series and alert policies; Cloud Trace; and Error Reporting.

1. **Enable the APIs** on the project: **Cloud Logging API** and **Cloud Monitoring API**.
   Enablement takes a minute to propagate.

2. **Create a service account** with exactly **Logs Viewer** (roles/logging.viewer) and
   **Monitoring Viewer** (roles/monitoring.viewer), and download its JSON key.

3. **Place the key** on the machine running Triagic, at an absolute path the desktop app
   can read, e.g. `/opt/triagic/gcp-sa.json`.

   That field is not limited to service-account keys. It reads any credentials JSON
   Google accepts: an **impersonated-service-account** config or a **workload-identity
   federation** config work equally well, and are the better answer if your organization
   avoids long-lived keys. Point the field at that file instead.

4. **Fill the form.**

   | Field                        | Required | What to put                                                                                                                                                   |
   | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
   | **Project ID**               | yes      | The project whose logs you want. API usage is billed and quota-checked against it.                                                                            |
   | **Service account key path** | no       | Absolute path to a service-account key, an impersonation config, or a federation config. Blank falls back to Application Default Credentials on that machine. |

5. **Verify.** Start-checked only: every tool requires a project-scoped argument, so
   there is no fixed call that could serve as a health check.

| If it reports                                      | It usually means                                                                                               |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `Could not determine credentials`                  | No Google credentials on that machine. Set the key path, or run `gcloud auth application-default login` there. |
| `has not been used in project`, `SERVICE_DISABLED` | The API isn't enabled on the project.                                                                          |
| `403` / `permission denied`                        | Missing Logs Viewer or Monitoring Viewer.                                                                      |

## Cloudflare [#cloudflare]

`cloudflare` · a **hosted endpoint**: the desktop talks HTTPS to Cloudflare's Workers
Observability MCP server. Nothing runs locally, so this needs outbound network access
rather than an installed runtime.

Overview, prompts and the tool list: [/integrations/cloudflare](/integrations/cloudflare).

Eight tools: the three query-side ones this integration exists for (logs and
analytics, plus field and value discovery), plus five more the hosted server has
grown since — three read Workers scripts (list a worker, get a worker, get a
worker's code), and two search Cloudflare's own documentation.

1. **Create an API token** at **dash.cloudflare.com → My Profile → API Tokens** (or
   Account → API Tokens for an account-scoped one), with two permissions: **Account →
   Workers Observability → Read**, and **Account → Workers Scripts → Read** — the
   second covers the worker-listing tools above and is easy to miss because only the
   Observability permission is mentioned in Cloudflare's own MCP docs.

   An **account-scoped** token needs a third permission: **Account → Account Settings →
   Read**. The server has to look up which account the token belongs to before it can
   query anything, and that lookup is what this permission allows. Without it,
   setup fails with a 403 that looks like the Observability permission is missing when it
   is not.

   A Global API Key is not accepted; it must be an API token. The key's headers are
   never read, so there is nothing about the key that can be corrected.

2. **Fill the form.**

   | Field         | Required | What to put      |
   | ------------- | -------- | ---------------- |
   | **API token** | yes      | The token above. |

3. **Verify.** Start-checked only. Both of the server's cheap tools require a structured
   query argument with its own time range, and a guessed shape would report a working
   integration as broken.

| If it reports         | It usually means                                                                                                                                                          |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` / error `10000` | Token rejected. Check it's an API token, not a Global API Key.                                                                                                            |
| `403` / error `9109`  | Valid token, missing permission. Add **Workers Observability → Read** and **Workers Scripts → Read**, and on an account-scoped token **Account Settings → Read** as well. |

## Vercel [#vercel]

`vercel` · a **hosted endpoint** at `https://mcp.vercel.com`, reached with your Vercel
access token as the bearer. Nothing runs locally.

Overview, prompts and the tool list: [/integrations/vercel](/integrations/vercel).

Covers deployments, build and runtime logs, projects, teams, Web Analytics and
documentation search.

> **Warning:** Writes are not blocked here
>
> A remote server takes no flags, and Vercel MCP's authenticated tools include project
> and deployment management. The token's own scope is the only boundary.

> **Warning:** Vercel requires OAuth for this endpoint, so expect a 401
>
> Vercel's published guidance for `mcp.vercel.com` (checked 2026-08-19) documents
> **only** a browser OAuth authorization flow, restricted to AI clients Vercel has
> reviewed and approved. No API-key or access-token authentication is documented for
> this endpoint. `vcp_…` tokens are documented for Vercel's REST API, not its MCP
> server.
>
> This integration sends a personal access token as the bearer anyway, because the
> OAuth flow needs a browser and callback URL the desktop app does not have. If Vercel
> refuses it (the documented behavior), the integration reports **degraded with a
> 401** on its first sync rather than failing quietly, and no field fixes it. Until
> Triagic ships OAuth support for remote servers, treat this entry as unavailable
> unless you have seen it come back `running`.

1. **Create an access token** at **vercel.com/account/tokens → Create Token**. Set its
   **Scope** to the team that owns the projects you want readable. A token scoped to
   your personal account can't see a team's projects. Set an expiry you are willing to
   rotate on.

2. **Optionally find the project scope.** The team-and-project pair from the dashboard
   URL, e.g. `acme-inc/storefront`. Setting it scopes the session to that one project;
   leaving it blank reaches everything the token can see.

3. **Fill the form.**

   | Field             | Required | What to put                                                        |
   | ----------------- | -------- | ------------------------------------------------------------------ |
   | **Access token**  | yes      | `vcp_…`                                                            |
   | **Project scope** | no       | `acme-inc/storefront`. Leading and trailing slashes are tolerated. |

4. **Verify.** Health check lists teams, an *authenticated* call. It is deliberately not
   the documentation search, which Vercel serves publicly and would pass for a token that
   reaches nothing.

| If it reports           | It usually means                                                                                            |
| ----------------------- | ----------------------------------------------------------------------------------------------------------- |
| `401` / `invalid_token` | Wrong or expired token, or the endpoint requires OAuth, per the callout above.                              |
| `403`                   | Token scoped to a personal account rather than the owning team.                                             |
| `404`                   | **Project scope** doesn't resolve. Use the team-and-project pair from the dashboard URL, or leave it blank. |

## AWS [#aws]

`aws` · runs `awslabs.aws-api-mcp-server` via **uvx**, locked to read-only operations
· the same package and spawn as [DynamoDB](/docs/integrations/databases#dynamodb),
but without the DynamoDB framing: `call_aws` runs any read-style AWS CLI command —
EC2, S3, Lambda, RDS, IAM, CloudTrail, ECS and the rest — that the credentials' IAM
policy allows, not just DynamoDB's. Use this entry over DynamoDB when you want the
agent to see more than one service; use both if you want the DynamoDB-specific
troubleshooting text as well.

Overview, prompts and the tool list: [/integrations/aws](/integrations/aws).

1. **Create an IAM user with a read-only policy scoped to what you want visible.** For
   broad access across services, attach the AWS managed policy **ReadOnlyAccess** (read
   access to nearly everything) or **SecurityAudit** (narrower, security-and-config
   focused). For a smaller footprint, write a policy naming just the
   `Describe*`/`List*`/`Get*` actions on the services you actually want the agent to
   see. Generate an access key for the user.

2. **Or point at a named profile instead.** Choosing *Named profile* reads the
   credential from the `~/.aws/config` of the machine running Triagic, which is the only
   way to reach assume-role, IAM Identity Center (SSO), `credential_process` or MFA. See
   [AWS CloudWatch](#aws-cloudwatch) for the profile shapes.

   This server also injects `--profile <name>` into every CLI command it generates, so
   the profile is what the command itself runs as, not only what the SDK signs with.

3. **Fill the form.**

   | Field                     | Required                     | What to put                                                                                                            |
   | ------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
   | **Authentication method** | no, defaults **Access keys** | *Named profile* for assume-role, SSO, `credential_process` or MFA.                                                     |
   | **AWS access key ID**     | yes, with *Access keys*      | `AKIA…`                                                                                                                |
   | **AWS secret access key** | yes, with *Access keys*      |                                                                                                                        |
   | **Session token**         | no, with *Access keys*       | Only for temporary (STS) credentials.                                                                                  |
   | **Profile name**          | yes, with *Named profile*    | The name inside `[profile …]` in that machine's `~/.aws/config`.                                                       |
   | **Region**                | yes, defaults `us-east-1`    | Most reads are per-region; a resource in a different region needs a second instance of this integration pointed there. |
   | **CA certificate path**   | no                           | Only where a TLS-intercepting proxy sits in front of the AWS endpoints.                                                |

4. **Verify.** No health check runs for this one: the underlying tool returns AWS errors
   as *successful* results with an error field, so a check could not tell a working
   credential from a denied one. It is start-checked only; the first real tool call is
   what proves the policy actually covers what you asked for.

| If it reports                                                                  | It usually means                                                                                                                                                                                |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A refusal naming a write operation                                             | This integration runs in read-only mode on purpose: writes are refused before they reach AWS. Only describe/list/get-style reads are available.                                                 |
| `InvalidClientTokenId`, `SignatureDoesNotMatch`, `UnrecognizedClientException` | The access key ID and secret aren't a matching pair, or the key is inactive.                                                                                                                    |
| `AccessDenied`, `not authorized to perform`                                    | The credentials are valid but the IAM policy doesn't allow that specific call. Widen the policy to **ReadOnlyAccess**/**SecurityAudit**, or add the specific `Describe*`/`List*`/`Get*` action. |
| `EndpointConnectionError`, could not connect to the endpoint                   | **Region** isn't a real region code (`eu-west-1`, not "Ireland"), or this machine can't reach `amazonaws.com`.                                                                                  |

## Azure [#azure]

`azure` · runs the official Azure MCP Server (`@azure/mcp`) via **npx** in namespace
mode with `--read-only` and no `--namespace` filter · one router tool per Azure
service — compute, storage, sql, cosmos, aks, appservice, monitor and around thirty
more — each taking a `command` and listing its own read-only commands with
`learn: true`. Key Vault is deliberately left out: its read-only tree includes
`secret get` and `key get`, the values themselves, and a support-investigation agent
should never be one injected ticket away from dumping a vault. Same credential shape
as [Azure Monitor](#azure-monitor), broader role.

Overview, prompts and the tool list: [/integrations/azure](/integrations/azure).

1. **Register an app** in Entra ID → **App registrations → New registration**, unless
   you're using the Azure CLI method below. Note its **Directory (tenant) ID** and
   **Application (client) ID**.

2. **Create a credential for it: a secret or a certificate.** A client secret
   (**Certificates & secrets → New client secret**) is quicker to set up but expires and
   takes the integration down silently when it does. A certificate
   (**Certificates & secrets → Certificates**, uploading a PEM or PFX you generated)
   outlives that problem. Or skip both and pick **Azure CLI sign-in on this machine**,
   which borrows whoever last ran `az login` there — nothing is stored, but it stops
   working when that sign-in lapses.

3. **Grant the service principal `Reader` on the subscription.** This is the one
   material difference from Azure Monitor's setup: that entry only needs Monitoring
   Reader and Log Analytics Reader, because it only reads logs and metrics. This entry's
   router tools reach every other service too, so the principal needs the
   subscription-wide **Reader** role, not a service-specific one.

4. **Fill the form.**

   | Field                       | Required                                           | What to put                                                                                                              |
   | --------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
   | **Authentication method**   | no, defaults **Service principal — client secret** | Certificate outlives a secret; Azure CLI sign-in stores no credential at all.                                            |
   | **Tenant ID**               | yes, with a service principal                      | Entra ID → App registrations → your app → Directory (tenant) ID.                                                         |
   | **Client ID**               | yes, with a service principal                      | The same app registration's Application (client) ID.                                                                     |
   | **Client secret**           | yes, with *client secret*                          |                                                                                                                          |
   | **Client certificate path** | yes, with *certificate*                            | Absolute path on the machine running Triagic. PEM or PFX; a PEM must contain the private key as well as the certificate. |
   | **Certificate password**    | no                                                 | Only for a password-protected PFX.                                                                                       |
   | **Subscription ID**         | yes                                                | The subscription the service principal was granted Reader on.                                                            |

5. **Verify.** Health check is `subscription_list`, a direct tool with no arguments that
   fails on a bad credential or a principal with no role anywhere.

| If it reports                                                     | It usually means                                                                                                                                                                                                  |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `az login`, `AzureCliCredential`, `Please run 'az login'`         | The Azure CLI sign-in on this machine lapsed or was never made. Run `az login` as the user Triagic runs as, or switch to a service principal.                                                                     |
| `AADSTS7000215`, invalid client secret, `AADSTS700016`            | Entra ID rejected the service principal. Check the client secret hasn't expired (they do, silently), and that tenant ID, client ID and secret all come from the same app registration.                            |
| `AADSTS700027`, a certificate error                               | The client certificate was refused. Check the path exists and is readable, that a PEM holds the private key as well as the certificate, and that this exact certificate is uploaded under Certificates & secrets. |
| `AuthorizationFailed`, `403`                                      | The service principal signed in but has no role on this subscription. Grant it **Reader**.                                                                                                                        |
| A refusal naming a specific command, "not available in read-only" | This integration runs with `--read-only`, so only list/get/show-style commands exist under each service. Call the service tool with `learn: true` to see what it offers.                                          |
| Subscription not found                                            | The subscription ID isn't visible to this service principal, or the principal is in a different tenant than the subscription.                                                                                     |

## Okta [#okta]

`okta` · runs `okta-mcp-server` via **uvx**.

Overview, prompts and the tool list: [/integrations/okta](/integrations/okta).

Built for "I can't log in" tickets: Triagic can look a user up, see their status, groups
and app assignments, and read the system log, including a summary of recent failed
sign-ins. It asks Okta for read scopes only, and the server drops every tool those scopes
don't cover before Triagic sees the list.

> **Info:** Key pair, not an API token
>
> Okta's server doesn't accept an SSWS API token. It signs in as an API Services app with a
> private key, which means no browser step and no token tied to a person.

1. **Create the app.** Admin Console → **Applications → Create App Integration → API
   Services**. Copy the **Client ID**.

2. **Set client authentication.** On the **General** tab, edit **Client Credentials**:
   choose **Public key / Private key** and turn off **Require Demonstrating Proof of
   Possession (DPoP) header in token requests**. The server doesn't send DPoP.

3. **Add a key.** In **Public keys**, **Add key → Generate new key**. Save the private key
   as PEM and copy the **KID** shown next to it.

4. **Grant scopes.** On **Okta API Scopes**, grant `okta.users.read`, `okta.groups.read`,
   `okta.apps.read` and `okta.logs.read`. All four are requested, so a missing one fails
   the sign-in.

5. **Assign a role.** On **Admin roles**, assign **Read-Only Administrator**. API service
   apps can't read anything without an admin role, and this one can't write either.

6. **Fill the form.**

   | Field                 | Required | What to put                                                 |
   | --------------------- | -------- | ----------------------------------------------------------- |
   | **Org URL**           | yes      | `https://acme.okta.com`. The `-admin` address is converted. |
   | **Client ID**         | yes      | From the app's General tab.                                 |
   | **Key ID (kid)**      | yes      | The KID next to the public key.                             |
   | **Private key (PEM)** | yes      | The whole PEM, including the BEGIN and END lines.           |

7. **Verify.** Health check lists groups. A wrong key fails earlier, when the server starts.

| If it reports                         | It usually means                                                                                                                                                                                                      |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_dpop_proof` / `DPoP`         | Require DPoP is still on for the app.                                                                                                                                                                                 |
| `invalid_scope`                       | One of the four scopes isn't granted on Okta API Scopes.                                                                                                                                                              |
| `invalid_client` / `client_assertion` | The KID and private key don't pair, or client authentication isn't set to public key.                                                                                                                                 |
| `E0000006` / `403`                    | The app has no admin role. Assign Read-Only Administrator.                                                                                                                                                            |
| `E0000011` `Invalid token provided`   | A cached token for another org or app was reused. The server caches its token in this machine's keychain under one shared name, `OktaAuthManager`; remove that entry and restart. One Okta org per machine avoids it. |

The access token lives in this machine's keychain, outside Triagic's encrypted store,
and stays there after you remove the integration. Delete the `OktaAuthManager` entry
if you need it gone.
