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

# Connect your own MCP servers

> Bring MCP servers HQ doesn't ship in its catalog — a public URL via a custom integration, or a server inside your private network via an outbound mTLS connector.

The [Integrations](/guides/integrations) catalog covers the MCP servers HQ ships. When you have your **own** MCP server, there are two ways to bring it in, both ending in a private entry in your workspace catalog that you attach to agents like any other:

* **Custom MCP** — your server is reachable at a **public URL**. Probe it, then add it; HQ detects its auth and connects.
* **Connector** — your server lives **inside a private network** with no public ingress. A small connector agent on your side dials out over an mTLS tunnel, and HQ routes tool calls back through it.

Both are **admin-only** and scoped to your own workspace. All requests use the base URL `https://api.hq.zone` and a personal access token with the `admin` scope: `Authorization: Bearer hq_pat_...`. The servers you add are **private (T3)** catalog entries — visible only to your workspace.

## Custom integration policy

Both paths are gated by a workspace-wide policy enforced **before any outbound call**. Read it with `GET /v1/mcp/custom/policy` and set it with `PUT`:

```bash theme={null}
curl -X PUT https://api.hq.zone/v1/mcp/custom/policy \
  -H "Authorization: Bearer hq_pat_..." -H "Content-Type: application/json" \
  -d '{
        "custom_mcp_enabled": true,
        "allowed_domains": ["mcp.acme.com"],
        "blocked_domains": [],
        "require_approval": true
      }'
# → 200 (the updated policy)
```

* `custom_mcp_enabled` (required) — master switch; when `false`, every add/probe is refused.
* `allowed_domains` — if non-empty, only these hosts may be connected (an allowlist).
* `blocked_domains` — hosts that are always refused.
* `require_approval` — when `true`, a newly added server is held for an admin to approve before agents can see it.

References: [Get custom policy](/api-reference/integrations/mcp-custom-policy-get), [Update custom policy](/api-reference/integrations/mcp-custom-policy-put).

## Add a public MCP server (custom)

<Steps>
  <Step title="Probe the URL (dry run)">
    `POST /v1/mcp/custom/probe` connects to the server, detects its authentication, and previews its tools — **without writing anything**. It's checked against your policy first.

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/mcp/custom/probe \
      -H "Authorization: Bearer hq_pat_..." -H "Content-Type: application/json" \
      -d '{ "url": "https://mcp.acme.com/mcp" }'
    # → 200 {
    #   "endpoint": "https://mcp.acme.com/mcp",
    #   "auth": "none", "auth_label": "No authentication",
    #   "connectable": true,
    #   "server_name": "Acme Tools", "tool_count": 7,
    #   "tools": [ { "name": "lookup_order", "description": "..." } ],
    #   "note": null
    # }
    ```

    `auth` is one of `none`, `api_key`, `oauth`, or `oauth_unsupported`. `connectable` is `true` when you can finish setup self-serve (open servers, API-key servers, and OAuth servers that advertise dynamic client registration with a public client). When it's `false`, `note` explains why — typically an OAuth server that needs an operator to register a confidential client. A blocked/invalid URL or a failed discovery returns `400`. See [Probe a custom MCP](/api-reference/integrations/mcp-custom-probe).
  </Step>

  <Step title="Add it">
    `POST /v1/mcp/custom` re-probes, enforces policy, and creates the private catalog entry.

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/mcp/custom \
      -H "Authorization: Bearer hq_pat_..." -H "Content-Type: application/json" \
      -d '{ "url": "https://mcp.acme.com/mcp", "display_name": "Acme Tools" }'
    # → 200 { "slug": "t:<TENANT>:1", "display_name": "Acme Tools",
    #         "credential_model": "none", "next": "connected" }
    ```

    The body takes `url` (required), an optional `display_name`, and an optional `max_calls_per_day` cap. The `next` field tells you what's left:

    * `connected` — done; an open server is enabled immediately (or held for review if your policy requires approval).
    * `api_key` — finish by storing a key with the [Set MCP key](/api-reference/integrations/set-mcp-key) flow on the returned slug (see the [Integrations guide](/guides/integrations)).
    * `oauth_start` — finish the OAuth connect (see the [OAuth guide](/guides/oauth)).

    A non-connectable server, a policy block, or a failed discovery returns `400`. See [Add a custom MCP](/api-reference/integrations/mcp-custom-create).
  </Step>

  <Step title="Approve, if held for review">
    When your policy sets `require_approval`, a new server lands in `review_required` and stays hidden from agents until an admin approves it with `POST /v1/mcp/custom/{slug}/approve`. The same endpoint re-approves a server after its tool list changes (a tool-poisoning guard re-freezes it on schema drift).

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/mcp/custom/t:<TENANT>:1/approve \
      -H "Authorization: Bearer hq_pat_..."
    ```

    See [Approve a custom MCP](/api-reference/integrations/mcp-custom-approve).
  </Step>
</Steps>

<Tip>Set or change a per-day call cap any time with `PUT /v1/mcp/custom/{slug}/limits` — body `{ "max_calls_per_day": 500 }` (`null` or `0` = uncapped). See [Set custom MCP limits](/api-reference/integrations/mcp-custom-limits).</Tip>

Remove a custom server with `DELETE /v1/mcp/custom/{slug}` — see [Delete a custom MCP](/api-reference/integrations/mcp-custom-delete).

## Connect a private-network server (connector)

When the server has no public URL, register a **connector**: HQ mints a one-time enrollment token, your connector agent dials out and establishes a mutually-authenticated (mTLS) tunnel, and HQ routes tool calls to the private server through it.

<Steps>
  <Step title="Create the connector">
    `POST /v1/mcp/connectors` registers a pending connector and returns a one-time enrollment token plus the tunnel address your agent dials.

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/mcp/connectors \
      -H "Authorization: Bearer hq_pat_..." -H "Content-Type: application/json" \
      -d '{ "name": "datacenter-1" }'
    # → 200 {
    #   "connector_id": "<CONNECTOR_ID>",
    #   "enrollment_token": "<TOKEN>",
    #   "tunnel_addr": "tunnel.hq.zone:8091",
    #   "tunnel_server_name": "tunnel.hq.zone"
    # }
    ```

    <Warning>The `enrollment_token` is shown **once** and never stored — only its hash is kept. It is **single-use** and **expires in one hour**. If you lose it or it expires, create a new connector.</Warning>

    See [Create a connector](/api-reference/integrations/connector-create).
  </Step>

  <Step title="Enroll the connector agent">
    On your network, run the HQ connector agent with the token. It generates a keypair and a certificate signing request and calls `POST /v1/mcp/connectors/enroll` (public, authenticated by the one-time token — no session), receiving a CA-signed client certificate, the CA certificate, and the tunnel address. It then dials `tunnel_addr` over mTLS and stays connected.

    A missing, invalid, expired, or already-used token returns `403`. This call is made by the connector software, not by you directly. See [Enroll a connector](/api-reference/integrations/connector-enroll).
  </Step>

  <Step title="Register the private MCP behind it">
    Once the tunnel is up, expose the private MCP server reachable through it with `POST /v1/mcp/connectors/{id}/mcp`. This creates the catalog entry and installs it (enabled) for your workspace.

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/mcp/connectors/<CONNECTOR_ID>/mcp \
      -H "Authorization: Bearer hq_pat_..." -H "Content-Type: application/json" \
      -d '{ "display_name": "Internal Tools", "path": "/mcp" }'
    # → 200 { "slug": "t:<TENANT>:2" }
    ```

    `display_name` is required; `path` is the MCP's path on the connector's local base (default `/mcp`); `max_calls_per_day` is an optional cap. A revoked connector returns `400`; an unknown one returns `404`. See [Register a private MCP behind a connector](/api-reference/integrations/connector-add-mcp).
  </Step>
</Steps>

List your connectors with `GET /v1/mcp/connectors` — each reports `id`, `name`, `status` (`pending`, `enrolled`, `revoked`), `last_seen_at`, and `created_at` ([List connectors](/api-reference/integrations/connector-list)).

Revoke one with `DELETE /v1/mcp/connectors/{id}` — it clears the certificate fingerprint and any pending token so no new tunnel sessions can be established, and live sessions drop on their next reconnect (returns `204`). See [Revoke a connector](/api-reference/integrations/connector-revoke).

## Attach it to an agent

Adding a custom MCP or connector MCP makes it available to the **workspace**. To put it in front of a specific agent, add its slug to the agent's MCP subset:

```bash theme={null}
curl -X PUT https://api.hq.zone/v1/agents/<AGENT_ID>/mcp \
  -H "Authorization: Bearer hq_pat_..." -H "Content-Type: application/json" \
  -d '{"enabled": ["t:<TENANT>:1"]}'
```

See [Attach skills and MCP servers](/guides/agents).
