> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gominerva.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Minerva MCP

> How to reach Minerva from coding agents over the hosted MCP server, including per-harness setup, personal access tokens, scopes, and admin enablement.

export const StorybookFrame = ({storyId, title, version = "20260419-1", height, zoom = 0.7, preferLocalPreview = false}) => {
  const HOSTED_STORYBOOK_BASE_URL = "https://minerva-storybook.s3.ca-central-1.amazonaws.com/iframe.html";
  const isLocalDocsPreview = typeof window !== "undefined" && ["localhost", "127.0.0.1"].includes(window.location.hostname);
  const storybookBaseUrl = preferLocalPreview && isLocalDocsPreview ? "http://localhost:6006/iframe.html" : HOSTED_STORYBOOK_BASE_URL;
  const params = new URLSearchParams({
    id: storyId,
    viewMode: "story",
    singleStory: "true",
    shortcuts: "false",
    toolbar: "0",
    nav: "0",
    panel: "0"
  });
  params.set("v", version);
  const src = `${storybookBaseUrl}?${params.toString()}`;
  const unscaledHeight = `${Math.ceil(height / zoom)}px`;
  const unscaledWidth = `${100 / zoom}%`;
  const containerRef = useRef(null);
  const [failed, setFailed] = useState(false);
  useEffect(() => {
    retainFocusGuard();
    return releaseFocusGuard;
  }, []);
  useEffect(() => {
    if (failed) return undefined;
    const container = containerRef.current;
    if (!container || typeof document === "undefined") return undefined;
    let detach;
    const attach = iframe => {
      let loadCount = 0;
      let loadTimeoutId;
      const onLoad = () => {
        loadCount += 1;
        clearTimeout(loadTimeoutId);
        if (loadCount > 1) setFailed(true);
      };
      const startLoadTimeout = () => {
        if (loadTimeoutId || loadCount > 0) return;
        loadTimeoutId = setTimeout(() => {
          if (loadCount === 0) setFailed(true);
        }, LOAD_TIMEOUT_MS);
      };
      iframe.addEventListener("load", onLoad);
      let observer;
      if (typeof IntersectionObserver === "function") {
        observer = new IntersectionObserver(entries => {
          if (entries.some(entry => entry.isIntersecting)) startLoadTimeout();
        }, {
          rootMargin: "200px"
        });
        observer.observe(container);
      } else {
        startLoadTimeout();
      }
      return () => {
        iframe.removeEventListener("load", onLoad);
        clearTimeout(loadTimeoutId);
        if (observer) observer.disconnect();
      };
    };
    const tryAttach = () => {
      if (detach) return;
      const iframe = container.querySelector("iframe");
      if (iframe) detach = attach(iframe);
    };
    tryAttach();
    const mutations = new MutationObserver(tryAttach);
    mutations.observe(container, {
      childList: true,
      subtree: true
    });
    return () => {
      mutations.disconnect();
      if (detach) detach();
    };
  }, [failed]);
  const frameStyle = {
    height: `${height}px`,
    overflow: "hidden",
    border: "1px solid #e2e8f0",
    borderRadius: "12px",
    backgroundColor: "#ffffff",
    display: "flex",
    justifyContent: "center",
    alignItems: "flex-start"
  };
  if (failed) {
    const messageOffset = Math.min(Math.max((height - 57) / 2, 0), 140);
    return <div style={{
      width: "100%"
    }}>
        <div style={{
      ...frameStyle,
      alignItems: "flex-start",
      backgroundColor: "#f8fafc",
      color: "#475569",
      textAlign: "center",
      padding: "0 24px"
    }}>
          <div style={{
      marginTop: `${messageOffset}px`
    }}>
            <div style={{
      fontWeight: 600,
      marginBottom: "4px"
    }}>{title}</div>
            <div style={{
      fontSize: "0.875rem"
    }}>
              This screenshot did not load. Refresh the page to try again.
            </div>
          </div>
        </div>
      </div>;
  }
  return <div style={{
    width: "100%"
  }} ref={containerRef}>
      <div style={frameStyle}>
        <iframe src={src} title={title} loading="lazy" style={{
    width: unscaledWidth,
    height: unscaledHeight,
    transform: `scale(${zoom})`,
    border: "0",
    display: "block",
    transformOrigin: "top center",
    flexShrink: 0
  }} allowFullScreen></iframe>
      </div>
    </div>;
};

The hosted Minerva MCP server lets a coding agent such as Codex, Claude Code, Kimi, or opencode call Minerva directly. You register one URL in the agent's configuration, run that agent's login command, approve the request in the browser, and the agent stores its own token. There are no hand-pasted credentials in the normal flow.

<Warning>
  Hosted MCP access is **off by default**. Someone with the **Admin** role or
  above has to enable it for your organization, and choose which permissions
  agent tokens may carry, before anyone can connect. See [Admin
  Enablement](#admin-enablement).
</Warning>

**Access:** Enabling hosted MCP for an organization requires the **Admin** role or above. In the sidebar, go to **Administration** > **Configuration**, then open **Authentication** and find **Hosted MCP access**.

**If you are connecting an agent:** [Connect Your Coding Agent](#connect-your-coding-agent) · [Personal Access Tokens](#personal-access-tokens) · [Troubleshooting](#troubleshooting)

**If you administer an organization:** [Admin Enablement](#admin-enablement) · [Scopes](#scopes)

Use this guide when you need to:

* connect Codex, Claude Code, Kimi, or opencode to Minerva
* authenticate a headless or CI automation that cannot open a browser
* understand what the hosted Minerva MCP server exposes and at which URLs
* choose the right scopes for an agent instead of granting everything
* enable hosted MCP for an organization and decide what agent tokens may do
* diagnose why an agent cannot connect or cannot see the data it expects

<Info>
  MCP access is a per-user credential, not an application key. The organization,
  the token's workspace reach, and its permissions all come from the token
  itself, so an agent can only ever do what the person who authorized it may do.
  Minerva never accepts an application API key on the MCP endpoints.
</Info>

## Before You Begin

* confirm someone with the **Admin** role or above has enabled the hosted MCP token policy and allowed the scopes your agents need
* confirm your own team role meets the **Minimum team role** set in that policy
* install a coding agent that supports remote MCP servers over HTTP
* decide the token's **reach**: one workspace, or every workspace in the organization. A pinned token can only ever act in the one it names.
* for headless use, prepare a secrets manager to hold a personal access token

## Key Concepts

| Concept                         | What it means                                                                                                                                                                                                                                            |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **MCP**                         | Model Context Protocol. The convention coding agents use to discover and call external tools. Minerva hosts a server, so no local process is required.                                                                                                   |
| **Resource URL**                | The MCP endpoint an agent connects to. This is the value you register in the agent's configuration.                                                                                                                                                      |
| **Authorization server**        | The OAuth 2.1 endpoints that issue agent tokens. Minerva serves these on the same hostname as the MCP endpoint, so there is only ever one host to allowlist.                                                                                             |
| **Scope**                       | One named permission on a token, such as `screening:search`. A token carries an explicit list of them.                                                                                                                                                   |
| **Downscoping**                 | Minerva grants the overlap of what the agent asked for, what the tenant policy allows, and what your role and products permit. Asking for more grants less.                                                                                              |
| **Hosted MCP access policy**    | The per-organization setting deciding whether MCP tokens may be created at all, the minimum team role, and the full set of scopes any token may ever carry.                                                                                              |
| **Personal access token (PAT)** | A bearer token for automations that cannot open a browser. Draws on the same scope catalog, minus the two scopes only browser consent can grant.                                                                                                         |
| **Entitlement**                 | Product access granted to an organization, sometimes per workspace rather than across all of it: ID verification is organization-wide, the agent risk assessment beta is per workspace. An entitlement is not a permission, but some scopes require one. |

## The Minerva MCP Server

Minerva publishes two resource URLs on the same hostname:

| Endpoint                                 | Use it for                                                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `https://mcp.gominerva.com/mcp`          | The full server. Whatever the token's scopes permit, including writes.                           |
| `https://mcp.gominerva.com/mcp/readonly` | The read-only tool subset. Mutating tools are not advertised at all, whatever the token carries. |

Prefer the read-only endpoint whenever an agent only needs to look things up. It removes the possibility of an accidental write even if a token is broader than it should be, which makes it the right default for an autonomous agent.

You can register both. Most clients allow more than one MCP server, so a common setup is a `minerva` entry pointing at the full endpoint for day-to-day work and a `minerva-readonly` entry for agents you want structurally unable to change anything.

Minerva also serves the OAuth 2.1 authorization server on `https://mcp.gominerva.com`, so the discovery documents, the authorize and token endpoints, and the signing keys all live on the same host as the MCP endpoint. If your network policy requires an allowlist, one hostname is enough.

<Note>
  Compliant clients discover all of this automatically from
  `https://mcp.gominerva.com/.well-known/oauth-protected-resource` and
  `https://mcp.gominerva.com/.well-known/oauth-authorization-server`. You should
  never need to configure an authorization URL, a client ID, or a client secret
  by hand: Minerva supports dynamic client registration, so the agent registers
  itself on first login.
</Note>

## Connect Your Coding Agent

Setup is the same two steps everywhere: register the resource URL, then run the client's login command and approve the request in the browser.

### Step 1: Register The Server

Codex, Claude Code, and opencode each write their own manifest entry from the command line. Kimi has no MCP subcommand, so its entry is added to `~/.kimi-code/mcp.json` by hand.

<CodeGroup>
  ```bash Codex theme={null}
  codex mcp add minerva --url https://mcp.gominerva.com/mcp
  ```

  ```bash Claude Code theme={null}
  claude mcp add --transport http minerva https://mcp.gominerva.com/mcp
  ```

  ```bash opencode theme={null}
  opencode mcp add minerva --url https://mcp.gominerva.com/mcp
  ```

  ```json Kimi theme={null}
  {
    "mcpServers": {
      "minerva": {
        "url": "https://mcp.gominerva.com/mcp"
      }
    }
  }
  ```
</CodeGroup>

<Note>
  `codex mcp add` just writes `~/.codex/config.toml`. If you manage your
  configuration in dotfiles, add the equivalent block directly instead:

  ```toml theme={null}
  [mcp_servers.minerva]
  url = "https://mcp.gominerva.com/mcp"
  ```
</Note>

For a read-only agent, use the same commands with a different name and `https://mcp.gominerva.com/mcp/readonly` as the URL. That endpoint advertises only the read-only tool subset, which is the safer default when you are wiring Minerva into an autonomous agent.

### Step 2: Sign In

<CodeGroup>
  ```bash Codex theme={null}
  codex mcp login minerva
  ```

  ```bash Claude Code theme={null}
  claude mcp login minerva
  ```

  ```bash opencode theme={null}
  opencode mcp auth minerva
  ```
</CodeGroup>

**Kimi** has no login subcommand. It starts the browser authorization flow the first time the agent connects to the server, so just use Minerva in a session.

Each command opens your browser. If you are already signed in to `app.gominerva.com`, you only have to approve the request.

<Warning>
  **Codex:** hosted MCP login needs codex-cli **0.147.0-alpha.2 or later**. As
  of July 2026, that is published on npm's `alpha` channel only; no stable
  release carries it yet. On a stable client (0.146.0 or earlier; check with
  `codex --version`) the browser approval completes and the login still fails in
  the terminal with *Authorization server response missing required issuer*,
  because the client enforces the issuer requirement without reading the issuer
  from its own callback. That is a client bug, not a Minerva one: until 0.147.0
  is stable, [use a personal access token](#personal-access-tokens) with Codex
  instead.
</Warning>

### Step 3: Approve The Request

The consent screen names the client that asked, the workspace the token will act in, and every scope requested. `mcp:read` is preselected and cannot be deselected, because a token without it cannot discover the tools it may call.

<StorybookFrame storyId="pages-hosted-mcp-consent--default" title="MCP Consent Screen" height={1110} version="20260728-1" />

Deselect anything the agent does not need. [Scopes](#scopes) explains what each one grants. This is the last point at which you choose, and it is per connection, so a research agent and a case-management agent can hold very different permissions under the same account.

After you approve, Minerva confirms the grant and sends the authorization to your client immediately, then tells you to finish the login in your terminal. A CLI login has just opened its listener and is blocking on it, so the send lands while the client is waiting and the tab hands you back to the client's own confirmation. The figure below is a Codex login.

<StorybookFrame storyId="pages-hosted-mcp-consent--success-with-callback" title="MCP Connection Confirmed" height={318} version="20260728-1" />

The button on that screen is a fallback, not a step: if your client is still waiting, use it to send the authorization again; if your terminal has already printed success, the login is done and the page is simply left over. A client whose return address cannot be verified gets an error screen instead: **Authorization not sent**. The client is given no access; run the login command again.

## Personal Access Tokens

Two kinds of caller need a token rather than a browser sign-in: anything with no browser to open, such as CI jobs, schedulers, and containers, and a developer whose client cannot complete the browser login, which as of July 2026 means Codex on a stable release (see [Step 2](#step-2-sign-in)). For both, create a personal access token and send it as a bearer token. It draws on the same scope catalog as a browser sign-in, with one difference that matters: a personal access token cannot carry `mcp:write` or `offline_access`, which only browser consent can grant (see [Scopes](#scopes)).

Go to **Administration** > **Developers** and open **Personal access tokens**. The same section is on your account page.

Creating a token asks for:

* a **name**, so the token is identifiable later in the table
* an **expiry** in days, up to a maximum of 90
* **where this token can act**: one workspace, or every workspace in the organization
* the **scopes** it carries, offered as the intersection of the tenant policy, your team role, and your organization's entitlements

Check the scopes on offer against the work you actually want the agent to do. A token holding only `mcp:read`, which is all a default organization policy allows, connects and authenticates normally but can call just two introspection tools, `whoami` and `list_tool_permissions`: running searches needs `screening:search`, and reading profiles needs `screening:read`.

<StorybookFrame storyId="pages-hosted-mcp-personal-access-tokens--create-token" title="Create Personal Access Token" height={1140} version="20260728-1" />

The organization in this example has already allowed the screening scopes. In one that has not, this list shows **Connect and list available tools** on its own.

The plaintext token is shown exactly once. Copy it into your secrets manager before closing the dialog; Minerva stores only a hash and cannot show it again.

<StorybookFrame storyId="pages-hosted-mcp-personal-access-tokens--secret-shown-once" title="Personal Access Token Shown Once" height={355} version="20260728-1" />

Send it as a bearer token on the MCP endpoint. Every client sends the same header, whatever its own configuration looks like:

```http theme={null}
Authorization: Bearer <the token you copied>
```

Most clients set that header for you from their configuration rather than making you write it out. Point Codex at an environment variable holding the token you just copied, so the token itself never reaches the configuration file. Set the variable first, because both ways of registering the server below only name it:

```bash theme={null}
export MINERVA_MCP_TOKEN="<the token you copied>"
```

Then register the server. Either use the CLI:

```bash theme={null}
codex mcp add minerva \
  --url https://mcp.gominerva.com/mcp \
  --bearer-token-env-var MINERVA_MCP_TOKEN
```

That is the [Step 1](#step-1-register-the-server) command with one flag added.

Or, if you keep your configuration in dotfiles, write the equivalent block into `~/.codex/config.toml` by hand:

```toml theme={null}
[mcp_servers.minerva]
url = "https://mcp.gominerva.com/mcp"
bearer_token_env_var = "MINERVA_MCP_TOKEN"
```

Neither block holds the token. Both only name `MINERVA_MCP_TOKEN`, so whichever one you copy, the `export` above is the part that has to be in place.

`codex mcp list` then reports the server as `enabled` with `Bearer token` as its auth. That describes the configuration and not the credential: it reads the same whether or not `MINERVA_MCP_TOKEN` is set.

If the variable is not set in the environment the agent itself runs in, Codex connects nothing and every Minerva tool is missing with no error. Check it there rather than in the shell you registered from.

Do not run `codex mcp login` on top of this: the token is already the credential, so no browser flow is involved and the client-version problem above does not apply.

<Note>
  `codex mcp add` writes `bearer_token_env_var`, which names the variable rather
  than storing the token, so the token never reaches `~/.codex/config.toml`.
  Codex does also accept a literal `Authorization` value under `http_headers`,
  which puts the token in that file, so prefer the environment variable. Set it
  from your secrets manager where you can: typing the `export` by hand puts the
  live token in your shell history. Codex refuses the `bearer_token` key
  outright, with `bearer_token is not supported for streamable_http`. Whatever
  client you use, keep any file that holds a token out of source control.
</Note>

The token table then tracks each token's prefix, scopes, workspace reach, creation and last-used timestamps, and lets you revoke one without touching the others.

<StorybookFrame storyId="pages-hosted-mcp-personal-access-tokens--populated" title="Personal Access Tokens" height={310} version="20260728-1" />

<Warning>
  No Minerva MCP token lasts longer than **90 days**, so every automation needs
  a rotation plan before it goes live. A token also acts as you, for as long as
  it is valid: scope it as narrowly as the automation actually needs, bind it to
  a single workspace where possible, and revoke it the moment the automation is
  retired or its owner changes role.
</Warning>

A token that reaches the end of its expiry shows as **Expired**, not **Revoked**. Nobody has to have withdrawn your access for this to happen, so read the status before assuming someone did.

<StorybookFrame storyId="pages-hosted-mcp-personal-access-tokens--expired-token" title="Expired Personal Access Token" height={355} version="20260728-1" />

### When To Prefer A Token Over The Browser Flow

| Prefer a personal access token                                                           | Prefer the browser login                                               |
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| CI jobs, schedulers, and containers with no interactive browser                          | A developer working at a terminal on their own machine                 |
| A developer on a client whose browser login is broken, such as Codex on a stable release | A developer whose client can complete the browser login                |
| A client that does not implement OAuth dynamic client registration                       | Any client that supports remote MCP servers properly                   |
| An unattended automation with a fixed, audited scope set and a rotation schedule         | Short-lived, exploratory, or ad-hoc agent sessions                     |
| An environment where you must control credential rotation yourself                       | Cases where automatic token refresh is preferable to managing a secret |

## Admin Enablement

Hosted MCP access is **off by default**. Before anyone in an organization can connect an agent, someone with the **Admin** role or above has to turn it on and choose what agent tokens may do. This is the gate that matters, and it is almost always the reason a connection attempt fails.

Go to **Administration** > **Configuration** > **Authentication** and open **Hosted MCP access**.

Three settings matter:

* **Allow users to create hosted MCP personal access tokens** is the master switch for the organization; the other two settings only matter once it is on.
* **Minimum team role** is the lowest team role permitted to hold an MCP token. Defaults to **Developer**.
* **What tokens may do** is the complete set of scopes any token in this organization may ever carry, grouped by product area, and explained in [Scopes](#scopes). A new policy allows only `mcp:read`.

<StorybookFrame storyId="pages-hosted-mcp-admin-policy--default" title="Hosted MCP Access Policy" height={1185} version="20260728-1" />

This list is a ceiling, not a grant. When someone creates a token they choose from this list, narrowed again by their own team role and by the products your organization has. Allowing a scope here never gives it to anyone who could not otherwise hold it.

<Note>
  Leaving **Minimum team role** at **Member** while allowing administration
  scopes does not widen access, because each scope also carries its own role
  floor. The panel names any allowed scopes that sit above the policy minimum.
  That is informational, not an error.
</Note>

Start by allowing only the connection and screening read scopes, confirm one agent connects, then add scopes as real workflows need them. Because Minerva downscopes rather than failing, a narrow policy produces a working agent with fewer tools instead of a login error.

## Scopes

A token carries an explicit list of scopes. Most need a team role, and some also need an organization entitlement, with one wrinkle for the workspace-scoped ones, described under the table.

| Scope                                                             | Requires, and what it allows                                                                                              |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `mcp:read`                                                        | **Member.** Confirm which account and workspace the token acts in, and list the tools it can call.                        |
| `mcp:write`                                                       | **Browser consent only.** Write access across screening, risk, and identity verification. It never grants administration. |
| `screening:search`                                                | **Member.** Run new searches against sanctions, PEP, and adverse media. Searches count against your organization's usage. |
| `screening:read`                                                  | **Member.** Read search results, profiles, potential matches, and profile comments.                                       |
| `screening:write`                                                 | **Member.** Create and edit profiles, set profile and potential-match status, and add profile comments.                   |
| `risk:read`                                                       | **Member + agent risk beta.** Read risk assessments along with their tasks, subjects, and comments.                       |
| `risk:write`                                                      | **Member + agent risk beta.** Create and edit risk assessment tasks, subjects, and comments.                              |
| `idv:read`                                                        | **Member + ID verification.** Read identity verification sessions and the status of each check.                           |
| `idv:write`                                                       | **Member + ID verification.** Start, resend, and cancel identity verification sessions.                                   |
| `admin:team`                                                      | **Admin.** Read who belongs to this organization and the team role each person holds.                                     |
| <code style={{ whiteSpace: "nowrap" }}>admin:config\_read</code>  | **Admin.** Read workspace and organization configuration settings.                                                        |
| <code style={{ whiteSpace: "nowrap" }}>admin:config\_write</code> | **Admin.** Change workspace and organization configuration settings.                                                      |
| `offline_access`                                                  | **Browser consent only.** Stay connected without signing in again until access is withdrawn.                              |

**Browser consent only** means a client can request the scope during browser sign-in, but it can never be put on a personal access token. `mcp:write` does not bypass anything: a write it covers still needs whatever entitlement the specific scope needs, so it reaches risk assessments only in an organization that has the agent risk assessment beta, and identity verification only where ID verification is enabled.

### How Scopes Are Narrowed

Minerva advertises the whole catalog above, and most agents request everything a server advertises on first login. That is expected and is not an error.

Minerva grants the intersection of:

1. what the agent requested,
2. what the tenant token policy allows,
3. what your team role and your organization's entitlements permit.

The token response reports the scopes that were actually granted. A Member in an organization that allows only screening scopes therefore ends up with a working connection that has screening tools and no administration tools. That is not a failed login. Check the granted scope list, not the requested one, when an expected tool is missing.

A few entitlements are granted per workspace rather than per organization (today the agent risk assessment beta), and the scopes that need one behave differently depending on the token's reach.

A token that acts in **every** workspace always carries `risk:read` and `risk:write` if the policy and your role allow them. There is no single workspace to check the beta against when the token is issued, so the check happens on each call instead, against the workspace that call names. The same token therefore succeeds in a workspace that has the beta and is refused in one that does not.

A token **pinned** to one workspace carries them only if that workspace holds the beta. So pinning does not get you more capability: it gets you less, and more predictably. The reason to pin is blast radius, not reach.

Refused risk calls are therefore about the workspace you are calling into, not about the token. See [Troubleshooting](#from-an-agent-call).

## Troubleshooting

### In The Browser

| Symptom                                                                                                   | Cause and fix                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| *Personal access tokens are disabled for this organization*                                               | Your administrator has not enabled Hosted MCP access. It is off by default. Someone with the **Admin** role or above turns it on under **Configuration** > **Authentication** > **Hosted MCP access**.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| *Personal access tokens require the … role or higher*                                                     | Your team role is below the policy's **Minimum team role**. Ask an Admin to lower the minimum, or to raise your team role.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| *Nothing in this organization’s token policy is available to you*                                         | The policy allows only scopes your role or your organization's products cannot carry. An Admin adds scopes you can hold, or Minerva enables the missing entitlement.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| The consent link fails before you can approve                                                             | The authorization request expired, or was already used because the login ran twice. Re-run the login command from the client so it starts a fresh request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `429` with an **empty** response body (`server: awselb/2.0`, no content-type)                             | The edge rate limit: 100 requests per 300 seconds per source IP, one bucket shared by `/oauth/register`, `/oauth/token`, and `/oauth/revoke`, any method. Blocked attempts still count toward the rate, so a tight loop that keeps re-flooding restarts the clock, and a slow one tells you nothing; left alone, the block lifts once the rate falls below the limit. Stop sending requests to those three paths for about five minutes, plus a short detection lag, then try **once**; an occasional attempt does not restart the clock. The limit can also engage a few minutes after you exceed it, so the block may appear later than the burst that caused it. |
| `429` with a **JSON** error body and an `x-request-id`                                                    | The application registration limit: 20 client registrations per source IP per hour, and it covers only `/oauth/register`. Reuse your existing client registration, or wait out the hour.                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Codex login fails with *Authorization server response missing required issuer* after the browser approval | A codex-cli bug, not a Minerva one: stable clients (0.146.0 or earlier) enforce the issuer requirement without reading the issuer from their own callback, and Minerva correctly sends it. Use a personal access token, or codex-cli 0.147.0-alpha.2 or later (currently the npm `alpha` channel).                                                                                                                                                                                                                                                                                                                                                                  |

Both rate limits key on your source IP, so a shared office or CI egress can trip them without anyone misbehaving. Neither one touches existing tokens, tool calls, discovery, or the authorization step itself: those keep working while new logins are throttled.

A greyed-out **Create token** button has two different causes, and the message on screen tells you which. This one shows *Personal access tokens are disabled for this organization*, meaning nobody here has hosted MCP access yet:

<StorybookFrame storyId="pages-hosted-mcp-personal-access-tokens--policy-disabled" title="Token Creation Blocked Because Hosted MCP Is Not Enabled" height={305} version="20260728-1" />

And this one shows *Personal access tokens require the … role or higher*, meaning the organization has enabled it but your own team role sits below the minimum the policy sets, which is a different fix:

<StorybookFrame storyId="pages-hosted-mcp-personal-access-tokens--role-floor-blocked" title="Token Creation Blocked By The Minimum Team Role" height={305} version="20260728-1" />

And this is the same organization from the admin's side, where the switch is turned off:

<StorybookFrame storyId="pages-hosted-mcp-admin-policy--tokens-disabled" title="Hosted MCP Access Policy, Default Off State" height={1125} version="20260728-1" />

A browser that opens the consent URL without a usable authorization request gets one of two screens, and which one tells you what went wrong.

If the request id is **well formed but no longer good** (the link expired, or it was already used because the login ran twice), you get the invalid-request screen. Re-run the login command from the client so it starts a fresh request.

<StorybookFrame storyId="pages-hosted-mcp-consent--expired-or-invalid" title="Expired Or Invalid Authorization Request" height={200} version="20260728-1" />

If there is **no request id at all, or a malformed one** (a hand-typed URL, or a link truncated in a chat message), you get an install landing page instead. It repeats the endpoints and carries copy-paste snippets for some clients, but it cannot complete a login on its own, and the commands earlier in this guide are the authoritative set.

<StorybookFrame storyId="pages-hosted-mcp-install-landing--default" title="MCP Install Landing" height={1135} version="20260728-1" />

### From An Agent Call

The three refusal codes an agent can meet (`validation_error`, `workspace_forbidden` and `entitlement_required`) are **authorization outcomes, not transport failures**. Minerva returns them as ordinary MCP tool results: HTTP `200` with `isError: true`, and the code, the message, and a `retryable` flag in the JSON inside `content[0].text`. A `401` therefore means authentication failed, while these mean the call was understood and refused. `validation_error` and `entitlement_required` are never retryable, and neither is a genuine `workspace_forbidden` refusal: the one retryable case is the service-problem form of `workspace_forbidden`, which says so in its message and sets `retryable: true`.

| Symptom                                                                                       | Cause and fix                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **No** Minerva tools at all, and no error                                                     | The client never connected, so no scope change will bring the tools back. On a personal access token using `bearer_token_env_var`, the usual cause is that the variable is not set in the environment the agent itself runs in. Check it there rather than in the shell you ran `codex mcp add` from. This is almost never downscoping: any token that carries `mcp:read` keeps `whoami` and `list_tool_permissions`, so if even those two are missing, scopes are not what removed them. |
| **Some** Minerva tools, but not the one you expected                                          | Downscoping: the connection succeeded and that scope was requested but not granted. Compare the granted scopes against the policy, your role, and your entitlements. If the policy is narrower than the work genuinely needs, ask an admin to widen it for that scope rather than in general.                                                                                                                                                                                             |
| `401` after the connection previously worked                                                  | The access token expired, and refresh only happens if `offline_access` was granted. Re-run the client's login command. For a personal access token, create a new one and update your secret.                                                                                                                                                                                                                                                                                              |
| The agent sees no data, or not the data you expected                                          | Check the token's reach. A pinned token acts only in the workspace it names, and a tool argument naming a different one is refused rather than honoured. A tenant-wide token acts wherever the call says, so look at the workspace the call named.                                                                                                                                                                                                                                        |
| *this token is not bound to a workspace, so workspace\_id is required …*                      | `validation_error`. The token acts in every workspace, so it cannot infer a target: each call has to name one. Expect this to be the first error an agent hits with a tenant-wide token, because nothing tells a model to pass a workspace until something does. `list_workspaces` is deliberately callable without a workspace so the agent can discover the ids, then retry with one named.                                                                                             |
| *workspace\_id does not match the workspace this token is bound to …*                         | `validation_error`. The token is pinned to one workspace and the call named a different one, which Minerva refuses rather than honours. Omit `workspace_id` so the pin applies, or use a token that is not bound to a single workspace.                                                                                                                                                                                                                                                   |
| *workspace\_id must be a valid workspace identifier …*                                        | `validation_error`. The call named something that is not shaped like a workspace id at all. The usual case is a model inventing one. Call `list_workspaces` and pick a real id.                                                                                                                                                                                                                                                                                                           |
| *workspace `<ID>` is not available to this token …*                                           | **`workspace_forbidden`, not retryable.** The named workspace is not one this token can act on. Minerva answers unknown, archived, and other-tenant workspaces identically on purpose, so this does not tell you which. Call `list_workspaces` to see what the token can reach. Retrying the same call gets the same answer.                                                                                                                                                              |
| *workspace access could not be verified just now; this is a service problem, not a refusal …* | **`workspace_forbidden`, retryable.** Minerva could not reach the entitlement service, so nothing was learned about the workspace either way, and the payload sets `retryable: true`. Retry the call; if it persists, the problem is on Minerva's side, not the token's.                                                                                                                                                                                                                  |
| *agent risk assessment … is not enabled for workspace `<ID>`*                                 | `entitlement_required`. The agent risk assessment beta is granted per workspace, and Minerva checks it on each call against the workspace that call names. The token is not the problem, and the same token can call the tool in a workspace that has the beta. Confirm the beta is enabled on that workspace, or direct the call at one that has it.                                                                                                                                     |
| The same not-enabled message on **every** workspace you try                                   | Also `entitlement_required`. A tenant-wide token is issued `risk:read` and `risk:write` without a workspace to check the beta against, so it can carry them in an organization where no workspace currently has it: most often one that had it, had an admin allow the scopes, and later lost it. The token is not broken. Ask an admin to confirm the beta is still enabled where you need it.                                                                                           |
| An application API key is rejected on the MCP endpoints                                       | Application keys are never accepted on MCP, by design. Use the browser login or a personal access token instead.                                                                                                                                                                                                                                                                                                                                                                          |

On opencode, two commands help isolate an authorization problem:

```bash theme={null}
# OAuth-capable servers and their auth status
opencode mcp auth list

# debug the OAuth connection for one server
opencode mcp debug minerva
```

## Best Practices

* use `https://mcp.gominerva.com/mcp/readonly` for any agent that does not need to write, instead of relying on scopes alone
* grant one connection per purpose rather than one broad connection reused everywhere, so revoking an agent does not disturb the others
* bind tokens to a single workspace unless an automation genuinely spans all of them
* keep the tenant policy as the ceiling you actually intend, and widen it in response to real workflows rather than in advance
* prefer the browser login for people, and reserve personal access tokens for automations that truly cannot open a browser
* store personal access tokens in a secrets manager, never in source control or in an agent configuration file committed to a repository
* schedule rotation before the 90-day ceiling rather than discovering it when a pipeline fails
* review the token table periodically and revoke tokens whose last-used timestamp shows they are no longer needed
* remember that an agent inherits its authorizer's permissions, so review team roles when someone changes job

## Related Guides

* [API Keys](/api-reference/api-keys)
* [SAML SSO and SCIM Guide](/authentication-sso-scim-guide)
* [Workspaces Guide](/workspaces-guide)
* [API Introduction](/api-reference/introduction)
