# Aperture connectors reference

Last validated Jul 21, 2026

> **Note:** Aperture connectors are currently in alpha.

\[Missing snippet: aperture\_mcp\_experimental\_note.mdx]

The `connectors` section of the [Aperture configuration][kb-aperture-configuration] configures outbound integrations with external services. Connectors support two protocols:

* **MCP** (`"protocol": "mcp"`): Aperture connects to remote [Model Context Protocol][xt-mcp-spec] servers, aggregates their tools and resources, and exposes them through the `/v1/mcp` endpoint.
* **HTTP** (`"protocol": "http"`): Aperture acts as an authenticated reverse proxy, forwarding requests to upstream HTTP APIs with credentials injected automatically.

Connectors replace the deprecated `mcp.servers` syntax and add support for authenticated connections and multiple protocols.

## Availability

Connectors are always available and the previous feature flag `connectors` is deprecated. Aperture still accepts it for backward compatibility, but it has no effect, so you can remove it from your configuration.

## `connectors` fields

The `connectors` section accepts the following top-level field:

| Field     | Type | Default | Description                                                                                                                                                                                                                        |
| --------- | ---- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `servers` | map  | `{}`    | Map of connector ID to server configuration. The map key is the connector ID, which becomes the name prefix for tools (`connectorID_toolname`), resources (`connectorID-uri`), and resource templates (`connectorID-uriTemplate`). |

## `servers` fields

Each entry in the `servers` map accepts the following fields:

| Field         | Type           | Required | Default      | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------- | -------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `protocol`    | string         | Yes      | N/A          | How Aperture communicates with the upstream. One of `"mcp"` or `"http"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `url`         | string         | No       | N/A          | Endpoint URL of the upstream server. A missing `url` produces a load-time warning rather than a fatal error, so Aperture still loads the configuration. The dashboard's connector save path, however, rejects it as an error.                                                                                                                                                                                                                                                                                                               |
| `provider`    | string         | No       | `"Custom"`   | Provider identifier. Set automatically when using a verified connector from the registry (for example, `"Salesforce"`, `"Atlassian"`, `"Slack"`). Custom connectors use the default value `"Custom"`. Controls whether the connector appears in the "verified" or "custom" category in the UI. If set manually, `provider` must exactly match a verified connector ID from the registry or be `"Custom"`. Any other value is a load-time error. Verified provider IDs are case-sensitive (for example, `"Salesforce"`, not `"salesforce"`). |
| `description` | string         | No       | unset (`""`) | Human-readable label surfaced to agents and the Aperture UI. When empty, the field is omitted from the stored configuration.                                                                                                                                                                                                                                                                                                                                                                                                                |
| `context`     | string or JSON | No       | N/A          | Prompt or context information to help models understand the connector. Accepts a plain string or arbitrary JSON object. Visible to all authenticated users through MCP tool discovery.                                                                                                                                                                                                                                                                                                                                                      |
| `auth`        | object         | No       | N/A          | Authentication configuration. Dispatched by the `type` field.                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

Example with both protocols:

```json
{
  "connectors": {
    "servers": {
      "tailnetMCP": {
        "protocol": "mcp",
        "url": "http://mcp-server.example.ts.net:8080/v1/mcp"
      },
      "github": {
        "protocol": "http",
        "url": "https://api.github.com",
        "description": "GitHub REST API (read-only)",
        "auth": {
          "type": "bearer_token",
          "secret": "github_pat_example"
        }
      }
    }
  }
}
```

In this example, `github` is a custom HTTP connector (its `provider` is unset, so it defaults to `"Custom"`). It is distinct from the verified `GitHub` MCP connector in the registry, which uses the case-sensitive provider ID `"GitHub"`.

## Connector ID rules

Connector IDs must match the pattern `[a-zA-Z][a-zA-Z0-9]*` (letters and digits only, starting with a letter). Connector IDs cannot contain underscores or hyphens because the MCP wire format reserves these as separators: underscores separate the connector ID from the tool name (`snowflake_query`), and hyphens separate the connector ID from resource URIs (`snowflake-resource://...`).

Valid IDs: `github`, `snowflake`, `analyticsV2`. Invalid IDs: `my-mcp` (hyphen), `github_api` (underscore), `2fast` (starts with digit).

The IDs `tailscale`, `internal`, and `aperture` are reserved and cannot be used as connector IDs. The `aperture` ID is used by the built-in system connector and produces a distinct error message from `tailscale` and `internal` (refer to [validation errors][ar-validation-errors]).

## System connectors

Aperture includes built-in system connectors that are always available, independent of the `connectors.servers` configuration. System connectors appear in the `GET /api/connectors` response with `category: "system"`.

| ID         | Provider | Protocol | Tools             | Description                                               |
| ---------- | -------- | -------- | ----------------- | --------------------------------------------------------- |
| `aperture` | Aperture | MCP      | `list_connectors` | Lists all HTTP API connectors available through Aperture. |

The `aperture` system connector helps AI models discover available HTTP connectors and construct requests to `/v1/connectors/<id>/<path>`. The `aperture` ID is reserved and cannot be used as a custom connector ID.

## Verified connectors registry

Aperture maintains a registry of verified connectors with pre-configured defaults for known providers. The registry is accessible through `GET /api/connectors-registry` and is used by the dashboard's connector picker to pre-fill configuration fields.

Each registry entry carries the provider's `url`, `auth_url`, `token_url`, `scopes`, and (where applicable) `auth_params`. The following row illustrates the shape of a single entry:

| Provider | Protocol | Auth type                   | URL                         | Scopes                                                                                |
| -------- | -------- | --------------------------- | --------------------------- | ------------------------------------------------------------------------------------- |
| Slack    | MCP      | `oauth2_authorization_code` | `https://mcp.slack.com/mcp` | `channels:read`, `channels:history`, `users:read`, `search:read.public`, `chat:write` |

Aperture ships more than a dozen verified connectors. Field values (including `auth_url`, `token_url`, `scopes`, and `auth_params`) change as providers update their endpoints, so query `GET /api/connectors-registry` for the authoritative current list rather than copying values from this page.

Verified connector defaults are starting points. Once an admin saves a connector, the stored configuration values are the authoritative source of truth. The registry values are not enforced at runtime.

When a connector's `provider` is set to any value other than `Custom`, the connector appears in the "verified" category in the dashboard UI. Custom connectors (with `provider: "Custom"` or unset) appear in the "custom" category.

## Connector categories

Each connector belongs to one of three categories based on its origin:

| Category   | Description                                                                         | Example                                      |
| ---------- | ----------------------------------------------------------------------------------- | -------------------------------------------- |
| `system`   | Built-in connectors provided by Aperture. Always present.                           | `aperture`                                   |
| `custom`   | Connectors configured by an admin with `provider: "Custom"` or no `provider` field. | `github`, `weather`                          |
| `verified` | Connectors whose `provider` is set to any value other than `Custom`.                | `salesforce` (with `provider: "Salesforce"`) |

In the dashboard UI, verified providers that are not yet configured appear as placeholders with the `not_configured` status, prompting admins to set them up.

## Connector statuses

The `GET /api/connectors` endpoint returns a `status` field for each connector indicating the user's authorization state:

| Status           | Meaning                                                                                                                                    | Resolution                                                                                                                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ready`          | Accessible and authorized. No action needed.                                                                                               | N/A                                                                                                                                                                 |
| `needs_auth`     | The user has grant access but has not completed the OAuth authorization flow.                                                              | Select **Connect** in the dashboard or call `POST /api/connectors/<id>/connect`.                                                                                    |
| `misconfigured`  | An OAuth token exists but lacks a refresh token. The OAuth application may not be configured for offline access.                           | Disconnect and reconnect. Make sure `auth_params` includes the provider-specific parameter for refresh tokens (for example, `"access_type": "offline"` for Google). |
| `no_access`      | The user does not have a matching `connectors` grant.                                                                                      | An admin must add a `connectors` grant for this user.                                                                                                               |
| `not_configured` | A verified registry provider that has not been configured by an admin. UI-only status (not returned by the API for configured connectors). | An admin must configure the connector in Settings.                                                                                                                  |

> **Note:**
>
> The statuses above report authorization state, not upstream health. A connector can be authorized yet still fail at request time if its upstream is unreachable. Use the **Connectors** page to confirm the connector can reach its upstream and list capabilities.

## Authentication

The `auth` object's `type` field selects the authentication scheme. The following table describes the available auth types:

| Type                        | Required fields                           | Description                                                                                                                                                                                                                                               |
| --------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bearer_token`              | `secret`                                  | Static `Authorization: Bearer <secret>` header.                                                                                                                                                                                                           |
| `api_key`                   | `secret`, `name`, `in`                    | Credential injected as a named header or query parameter. `in` must be `"header"` or `"query"`.                                                                                                                                                           |
| `basic`                     | `username`, `password`                    | HTTP Basic authentication.                                                                                                                                                                                                                                |
| `oauth2_client_credentials` | `client_id`, `client_secret`, `token_url` | Machine-to-machine OAuth 2.0 token. Auto-refreshed. Optional `scopes` array.                                                                                                                                                                              |
| `oauth2_authorization_code` | `client_id`, `auth_url`, `token_url`      | Per-user OAuth 2.0 consent flow. Optional `client_secret` (not required for PKCE-only), `scopes`, and `auth_params`.                                                                                                                                      |
| `oauth2_dcr`                | `url`                                     | Per-user OAuth 2.0 consent flow using [dynamic client registration][ar-dcr]. Aperture discovers the authorization server and registers a client at runtime, so no `client_id`, `auth_url`, or `token_url` is needed. Optional `scopes` and `auth_params`. |

### API key

```json
{
  "connectors": {
    "servers": {
      "weather": {
        "protocol": "http",
        "url": "https://api.weather.example.com",
        "auth": {
          "type": "api_key",
          "secret": "wk-example-key",
          "name": "X-API-Key",
          "in": "header"
        }
      }
    }
  }
}
```

The `in` field must be `"header"` or `"query"`. When set to `"header"`, Aperture injects the credential as a request header with the name specified in `name`. When set to `"query"`, Aperture appends it as a URL query parameter.

### Per-user OAuth 2.0 authorization code flow

When a connector uses `oauth2_authorization_code`, each user must complete an individual consent flow. Aperture uses PKCE with the S256 challenge method. Each user gets an isolated MCP client session, so backends that tie session state to a bearer token cannot mix up cross-user state.

#### Initiating the flow

The connect and disconnect endpoints apply only to connectors that use a per-user interactive OAuth flow (`oauth2_authorization_code` or [`oauth2_dcr`][ar-dcr]). Other connectors return a "not found" error.

To start authorization, send a POST request:

```
POST /api/connectors/<id>/connect
```

Aperture returns a JSON response containing the provider's authorization URL:

```json
{"auth_url": "https://provider.example.com/oauth2/authorize?client_id=...&code_challenge=..."}
```

The user must open this URL in a browser to complete the consent flow. After granting consent, the provider redirects the user to Aperture's callback at `/aperture/auth/<id>/callback`, which renders an HTML page confirming success or failure. Register this callback URL with your OAuth provider when creating the application.

Pending authorization flows expire after 15 minutes. If the user does not complete the consent flow within this window, the flow is rejected.

#### Token lifecycle

Aperture stores tokens per-user and treats them as expired 60 seconds before their actual expiry, triggering a refresh on the next request. If a refresh fails (for example, because the provider revoked the refresh token), Aperture deletes the stored credential and the user must re-authorize by calling `POST /api/connectors/<id>/connect` again.

#### Lazy population

Connectors using `oauth2_authorization_code` populate their tool catalog lazily: Aperture fetches tools on the first authenticated request from each user rather than at startup. This means tools from a per-user connector do not appear in `tools/list` until at least one user has authorized.

#### `auth_params`

The `auth_params` field appends custom parameters to both the authorization URL and the token exchange request body. Some providers require specific parameters to issue refresh tokens:

* **Google**: `{"access_type": "offline", "prompt": "consent"}`. Without `access_type: offline`, Google does not issue a refresh token.

\[Missing snippet: aperture\_google\_workspace\_dpp\_note.mdx]

```json
{
  "connectors": {
    "servers": {
      "google": {
        "protocol": "mcp",
        "url": "https://mcp.google.example.com/v1/mcp",
        "auth": {
          "type": "oauth2_authorization_code",
          "client_id": "example-client-id",
          "client_secret": "example-client-secret",
          "auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
          "token_url": "https://oauth2.googleapis.com/token",
          "scopes": ["https://www.googleapis.com/auth/spreadsheets.readonly"],
          "auth_params": {"access_type": "offline", "prompt": "consent"}
        }
      }
    }
  }
}
```

The `auth` block rejects unknown fields. A typo in a field name (for example, `"secert"` instead of `"secret"`) produces a validation error at load time.

### Dynamic client registration (oauth2\_dcr)

`oauth2_dcr` is a per-user OAuth 2.0 flow for MCP servers that support dynamic client registration ([RFC 7591][xt-rfc-7591]). Instead of registering an OAuth application yourself and supplying a `client_id`, `auth_url`, and `token_url`, Aperture discovers the authorization server from the connector's `url` ([RFC 9728][xt-rfc-9728] protected-resource metadata, then [RFC 8414][xt-rfc-8414] authorization-server metadata) and registers a client at runtime.

The only required field is `url`, the MCP server's resource URL. Optional `scopes` and `auth_params` are supported. Because nothing is registered at the provider ahead of time, a DCR connector has no client ID or secret.

A DCR connector uses the same per-user [connect flow][ar-connect-flow], [token lifecycle][ar-token-lifecycle], and [lazy population][ar-lazy-population] as `oauth2_authorization_code`, with one difference: every `oauth2_dcr` connector authorizes through a single shared callback, `https://<aperture-hostname>/aperture/auth/dcr/callback`, rather than a per-connector callback.

```json
{
  "connectors": {
    "servers": {
      "atlassian": {
        "protocol": "mcp",
        "url": "https://mcp.atlassian.com/v1/mcp/authv2",
        "auth": {
          "type": "oauth2_dcr",
          "url": "https://mcp.atlassian.com/v1/mcp/authv2"
        }
      }
    }
  }
}
```

### Bearer token

```json
{
  "connectors": {
    "servers": {
      "snowflake": {
        "protocol": "mcp",
        "url": "https://mcp.example.com/v1/mcp",
        "auth": {
          "type": "bearer_token",
          "secret": "sk-example-token"
        }
      }
    }
  }
}
```

### OAuth 2.0 client credentials

```json
{
  "connectors": {
    "servers": {
      "analytics": {
        "protocol": "mcp",
        "url": "https://analytics.example.com/v1/mcp",
        "auth": {
          "type": "oauth2_client_credentials",
          "client_id": "aperture-analytics",
          "client_secret": "secret-example",
          "token_url": "https://auth.example.com/oauth2/token",
          "scopes": ["read:data"]
        }
      }
    }
  }
}
```

## MCP connectors

When `protocol` is `"mcp"`, Aperture connects to remote MCP servers, aggregates their tools and resources, and exposes them through the `/v1/mcp` endpoint.

### Name prefixing

Each key in the `servers` map is a connector ID that Aperture uses as a name prefix:

* Aperture prefixes tools with the connector ID and an underscore. For example, a tool named `search` on the `docs` connector becomes `docs_search`.
* Resources use a hyphen instead of an underscore. For example, `docs-files://readme.md`.

Name prefixing prevents collisions when multiple servers expose tools with the same name. Clients receive the prefixed names and Aperture automatically strips the prefix when forwarding calls to the remote server.

### Transport auto-detection

Aperture automatically detects whether each remote MCP server supports Streamable HTTP (the current protocol) or legacy SSE. When connecting to a remote server, Aperture tries Streamable HTTP first and falls back to SSE if the server does not support it. You can upgrade remote servers to a later protocol version without restarting Aperture.

The same auto-detection applies to clients connecting to Aperture's `/v1/mcp` endpoint. Aperture serves both Streamable HTTP and legacy SSE clients.

### Capability polling

Aperture polls each configured remote MCP server every 5 seconds to detect capability changes. When a remote server adds or removes tools or resources, Aperture automatically updates the registrations, making the changes visible to connected clients.

If a remote server becomes unavailable, Aperture unregisters all of its tools and resources until the server recovers. Polling continues in the background, and Aperture re-registers capabilities when the server comes back online.

> **Note:**
>
> Connectors using `oauth2_authorization_code` authentication are an exception. They have no 5-second background poller because capabilities are per-user. Instead, Aperture loads a user's capabilities lazily when that user starts a session, and refreshes them when the user calls the capability refresh endpoint (`PUT /api/connectors/<id>/capabilities`).

### System connectors

Aperture includes a built-in `aperture` system connector that exposes the `list_connectors` tool. This tool lists all HTTP API connectors available through Aperture, helping AI models discover which APIs they can access and how to call them. To grant access, use a `connectors` grant matching `"aperture/**"` or `"aperture/tools/*"`.

The `aperture` system connector always appears in `GET /api/connectors`, but its `aperture_list_connectors` MCP tool is registered only when at least one HTTP connector is configured.

On the `/v1/mcp` endpoint, this tool appears with the connector-ID prefix as `aperture_list_connectors`, consistent with the name-prefixing rule. Grants reference the unprefixed tool name (for example, `"aperture/tools/list_connectors"`).

System connectors are not granted by default. The grant system governs their access the same way it governs custom and verified connectors. Being a system connector confers no special access. Without a matching `connectors` grant, users cannot access the `aperture` connector's tools.

> **Tip:**
>
> The Aperture host uses its tailnet HTTP client for connections to remote MCP servers, so you can use tailnet hostnames (for example, `http://mcp-server.example.ts.net:8080/v1/mcp`) without additional network configuration.

## HTTP connectors

When `protocol` is `"http"`, Aperture acts as an authenticated reverse proxy. Aperture forwards requests sent to `/v1/connectors/<id>/<path>` to the connector's upstream URL and injects credentials automatically. When at least one HTTP connector is configured, Aperture registers an `aperture_list_connectors` MCP tool so that AI models can discover available HTTP connectors.

> **Warning:**
>
> The authentication credentials configured on an HTTP connector are shared by all users with a matching `connectors` grant. Users must have a grant matching `"connectorID/proxy"` or a broader pattern like `"connectorID/**"` to access the proxy endpoint. Use the narrowest possible credential scope (ideally read-only) and do not put sensitive information in the `description` or `context` fields, which are visible to all users.

### Allowed HTTP methods

HTTP connectors accept the following request methods: GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS. All other methods (including TRACE and CONNECT) are rejected with `405 Method Not Allowed`. TRACE is blocked because it reflects request headers, which would expose injected authentication credentials.

### Request and response handling

Aperture modifies requests and responses passing through the HTTP connector proxy:

* **Request headers**: Aperture strips `Authorization` and `Cookie` headers from client requests before forwarding to the upstream. This prevents clients from injecting credentials that could leak to the upstream server. Aperture injects the correct credentials separately based on the connector's `auth` configuration.
* **Response headers**: Aperture strips `Set-Cookie` headers from upstream responses, preventing the upstream from setting cookies on Aperture's origin.
* **Hop-by-hop headers**: Standard hop-by-hop headers (`Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `Te`, `Trailer`, `Transfer-Encoding`, `Upgrade`) are stripped in both directions per HTTP specification.
* **Request body**: Forwarded as-is with no size limit.
* **Response body**: Limited to 50 MB. Responses exceeding this limit are truncated.

### Security behavior

HTTP connectors enforce the following security restrictions:

* **URL restrictions**: Connector URLs are validated against a safe dialer that blocks internal and reserved address ranges. RFC 1918 (private), ULA, and Tailscale CGNAT addresses are permitted.
* **Redirect blocking**: HTTP connectors block all redirects from upstream servers to prevent credential exfiltration through redirect chains.
* **Credential injection**: Aperture injects credentials on every proxied request based on the connector's `auth` configuration. Client-supplied `Authorization` headers are always replaced.

## Dynamic registration

> **Note:**
>
> Dynamic registration is configured only through the legacy `mcp` section and has no `connectors` equivalent. The behavior described here applies to the `mcp` section.

Dynamic registration lets MCP servers register themselves with Aperture at runtime instead of through static configuration. Set `accept_registrations` to `true` in the `mcp` section of your configuration. This field requires the `mcp` section and has no equivalent in `connectors`.

```json
{
  "mcp": {
    "accept_registrations": true,
    "servers": {}
  }
}
```

Remote servers register by sending a POST request to `/v1/mcp/register` with a JSON body containing their URL:

```shell
curl -X POST http://<aperture-hostname>/v1/mcp/register \
  -H "Content-Type: application/json" \
  -d '{"url": "http://my-mcp-server:8080/v1/mcp"}'
```

Aperture performs an initial capability fetch from the registering server, then responds with HTTP 200 and begins polling the server for capability changes. Each dynamically registered server receives a sequential ID (`auto1`, `auto2`, and so on), and Aperture prefixes its tools accordingly (for example, `auto1_search`).

> **Warning:**
>
> The registration endpoint requires Tailscale authentication, the same as all other Aperture endpoints. The registering server must be accessible through the tailnet.
>
> The registering server must keep the HTTP connection to `/v1/mcp/register` open. Aperture sends keepalive messages on this connection every second. When the server closes the connection, Aperture automatically unregisters all of its tools and resources.

You can combine static connectors and dynamic registration in the same configuration. Static connectors are always available, while dynamically registered servers come and go as they connect and disconnect.

### `mcp` fields

| Field                  | Type    | Default | Description                                                                                                                                                                                                                                                                         |
| ---------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accept_registrations` | boolean | `false` | Allow backends to register dynamically through `POST /v1/mcp/register`. Backends POST `{"url": "http://..."}` and keep the connection open. Tools are unregistered when the connection closes. This field has no `connectors` equivalent.                                           |
| `servers`              | map     | `{}`    | Map of server ID to server configuration. Deprecated: use `connectors.servers` instead. The map key is the server ID, which becomes the name prefix for tools (`serverID_toolname`), resources (`serverID-uri`), and resource templates (`serverID-uriTemplate`) from that backend. |

## Grants for connector resources

Aperture is deny-by-default. Without grants, users cannot access any connector capabilities. This applies to both MCP and HTTP connectors. Add grants in the `grants` section of the Aperture configuration.

Connector grants use the `connectors` field with `"connectorID/category/resource"` FQN glob patterns. The following example grants all users access to all tools from the `docs` connector and proxy access to the `github` HTTP connector:

```json
{
  "grants": [
    {
      "src": ["*"],
      "app": {
        "tailscale.com/cap/aperture": [
          {"connectors": ["aperture/**"]},
          {"connectors": ["docs/tools/*"]},
          {"connectors": ["github/**"]}
        ]
      }
    }
  ]
}
```

The `connectors` field accepts an array of FQN glob strings. Each pattern has up to three segments: `connectorID/category/resource`.

### Grant categories

| Category    | Applies to      | Description                                     | Example               |
| :---------- | :-------------- | :---------------------------------------------- | :-------------------- |
| `tools`     | MCP connectors  | MCP tools exposed by the connector              | `"docs/tools/search"` |
| `resources` | MCP connectors  | MCP resources exposed by the connector          | `"docs/resources/*"`  |
| `templates` | MCP connectors  | MCP resource templates exposed by the connector | `"docs/templates/*"`  |
| `proxy`     | HTTP connectors | Proxy access to the HTTP connector endpoint     | `"github/proxy"`      |

### Grant patterns

You can use `*` to match any characters within a segment and `**` to match across segments:

* `"docs/tools/search"`: the `search` tool from the `docs` connector.
* `"docs/tools/*"`: all tools from the `docs` connector.
* `"docs/**"`: all capabilities from the `docs` connector (tools, resources, templates).
* `"github/proxy"`: proxy access to the `github` HTTP connector.
* `"github/**"`: all access to `github` (proxy and any future categories).
* `"**"`: all capabilities from all connectors.

A top-level pattern like `"connectorID/*"` is automatically expanded to `"connectorID/**"`, so it covers all categories and sub-resources.

Aperture checks grants when clients list available tools and resources. Users can access only the items their grants permit. Aperture also enforces grants at invocation time: when a client calls a tool, reads a resource, or sends a proxy request, Aperture checks the session's grants before dispatching the request. If no grant matches, Aperture rejects the call with a "forbidden" or "unknown tool" error.

> **Note:**
>
> The deprecated `mcp_tools`, `mcp_resources`, and `mcp_templates` grant fields continue to work for backward compatibility with the `"server/item"` pattern syntax. New configurations should use the `connectors` field, which supports all connector types including HTTP proxy access.
>
> Resource grants are now matched against the resource's name, not its URI. Legacy `mcp_resources` patterns written against resource URIs no longer match. Rewrite them against the resource name using `connectors` with the `resources` category.

Refer to the [grants configuration reference][kb-aperture-grants] for the full grants syntax.

## Migration from `mcp.servers`

The `connectors` section replaces the deprecated `mcp.servers` syntax. At load time, Aperture automatically folds legacy `mcp.servers` entries into `connectors.servers` with `protocol: "mcp"` and no authentication. Both sections can coexist during migration, but connector IDs must be unique across both. Aperture rejects duplicate IDs at load time.

Legacy `mcp.servers` entries that use connector IDs not conforming to the `[a-zA-Z][a-zA-Z0-9]*` pattern generate a warning but are still accepted for backward compatibility. New entries in the `connectors` section must conform to the ID rules.

Consider the following `mcp.servers` entry:

```json
"mcp": { "servers": { "docs": { "url": "http://mcp-server.example.ts.net:8185/v1/mcp" } } }
```

The equivalent `connectors` entry is:

```json
"connectors": { "servers": { "docs": { "protocol": "mcp", "url": "http://mcp-server.example.ts.net:8185/v1/mcp" } } }
```

The `mcp` section is retained for backward compatibility and for the `accept_registrations` field, which has no `connectors` equivalent.

## Validation errors

Aperture validates connector configuration at load time. The following table describes connector-specific validation messages:

| Condition                                            | Message                                                                                                                                                                  | Severity |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| Connector missing `protocol`                         | `connectors.servers.{id}: protocol is required (e.g. "mcp" or "http")`                                                                                                   | Error    |
| Unsupported connector protocol                       | `connectors.servers.{id}: unsupported protocol "{value}" (supported: "mcp", "http")`                                                                                     | Error    |
| Invalid connector ID                                 | `connectors.servers.{id}: invalid ID "{id}"; connector IDs must match [a-zA-Z][a-zA-Z0-9]* (letters and digits only, starting with a letter; no hyphens or underscores)` | Error    |
| Reserved connector ID (`tailscale`, `internal`)      | `connectors.servers.{id}: "{id}" is a reserved identifier`                                                                                                               | Error    |
| Reserved system connector ID (`aperture`)            | `connectors.servers.{id}: "{id}" is a reserved system connector identifier`                                                                                              | Error    |
| Connector sets an unknown provider                   | `connectors.servers.{id}: unknown provider "{value}"; use a verified ID or omit for custom connectors`                                                                   | Error    |
| Connector missing URL                                | `connectors.servers.{id}: URL is required`                                                                                                                               | Warning  |
| Connector URL uses restricted address                | `connectors.servers.{id}: URL {details}`                                                                                                                                 | Warning  |
| Connector auth validation failure                    | `connectors.servers.{id}.auth: {details}`                                                                                                                                | Error    |
| Duplicate connector ID across `mcp` and `connectors` | `config: server ID "{id}" is defined in both 'mcp.servers' and 'connectors.servers'; IDs must be unique across both sections`                                            | Error    |

Refer to the [Aperture configuration reference][kb-aperture-configuration] for the full validation table.

## REST API endpoints

The following endpoints manage connectors at runtime. All endpoints require Tailscale authentication.

| Method | Path                                | Description                                                                                               | Access                  |
| ------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------- |
| `GET`  | `/api/connectors`                   | List all connectors (system, custom, verified) with status for the current user.                          | All authenticated users |
| `GET`  | `/api/connectors/{id}/capabilities` | Get cached MCP capabilities (tools, resources, templates) for a connector. Requires a `connectors` grant. | Grant required          |
| `PUT`  | `/api/connectors/{id}/capabilities` | Force-refresh capabilities from the upstream MCP server.                                                  | Grant required          |
| `POST` | `/api/connectors/{id}/connect`      | Start an OAuth 2.0 authorization code flow. Returns `{"auth_url": "..."}`.                                | Grant required          |
| `POST` | `/api/connectors/{id}/disconnect`   | Forget the current user's OAuth token for a connector.                                                    | Grant required          |
| `GET`  | `/api/connectors-registry`          | List the verified connectors registry (pre-configured providers).                                         | All authenticated users |

The HTTP connector proxy endpoint uses a separate path:

| Method        | Path                         | Description                                        | Access                             |
| ------------- | ---------------------------- | -------------------------------------------------- | ---------------------------------- |
| Any supported | `/v1/connectors/{id}/{path}` | Reverse proxy to an HTTP connector's upstream URL. | Requires `connectorID/proxy` grant |

## Full annotated example

The following example shows a complete connector configuration with both MCP and HTTP protocols, authentication, grants, and dynamic registration:

```json
{
  // Outbound integrations (MCP servers and HTTP APIs)
  "connectors": {
    "servers": {
      // Unauthenticated MCP server on your tailnet
      "tailnetMCP": {
        "protocol": "mcp",
        "url": "http://mcp-server.example.ts.net:8080/v1/mcp"
      },
      // MCP server with bearer token auth
      "snowflake": {
        "protocol": "mcp",
        "url": "https://mcp.example.com/v1/mcp",
        "auth": {
          "type": "bearer_token",
          "secret": "sk-example-token"
        }
      },
      // HTTP API proxy with OAuth 2.0 client credentials
      "analytics": {
        "protocol": "http",
        "url": "https://analytics.example.com/api",
        "description": "Analytics API (read-only)",
        "auth": {
          "type": "oauth2_client_credentials",
          "client_id": "aperture-analytics",
          "client_secret": "secret-example",
          "token_url": "https://auth.example.com/oauth2/token",
          "scopes": ["read:data"]
        }
      }
    }
  },

  // Dynamic MCP registration (accept_registrations has no connectors equivalent)
  "mcp": {
    "accept_registrations": true
  },

  // Grant all users access to Aperture built-in tools and all connectors
  "grants": [
    {
      "src": ["*"],
      "app": {
        "tailscale.com/cap/aperture": [
          {"connectors": ["aperture/**"]},
          {"connectors": ["**"]}
        ]
      }
    }
  ]
}
```

## Troubleshooting

Connector problems fall into two flows with different causes and fixes: an admin setting up and configuring a connector, and a user authorizing and using a connector's tools. Use the section that matches your role.

The **Connectors** page in the dashboard is the fastest first check for either role. It probes each connector's upstream URL and reports whether Aperture can reach the server and list its capabilities, so it surfaces most connection and configuration errors without requiring access to the Aperture host.

### For admins setting up connectors

These issues affect connector configuration and reachability. Start with the **Connectors** page to confirm Aperture can reach the upstream.

#### MCP tools do not appear

If tools from a configured MCP connector are not visible:

1. Open the **Connectors** page and check the connector's status and capability list. Because the page probes the upstream URL, it reports whether Aperture can reach the server and list its capabilities.
2. Verify the URL in your configuration is correct and the MCP server is running.
3. Verify your grants include `connectors` patterns that match the connector and tool names (for example, `"docs/tools/*"`). Without grants, users cannot access any connector tools.

#### Connection refused or host not found

These errors indicate the connector URL is unreachable. The **Connectors** page shows the error when it probes the upstream.

* **Connection refused**: The server is not running or is not listening on the configured port.
* **Host not found**: DNS cannot resolve the hostname. For tailnet hostnames, verify the device is connected to the tailnet and check with `tailscale status`.

#### Tools appear and then disappear

If tools are briefly visible and then become unavailable, the remote MCP server is likely crashing or restarting. Aperture automatically unregisters tools when a remote server becomes unreachable and re-registers them when the server recovers.

#### Auth configuration rejected with unknown field error

The `auth` block uses strict field validation. If a field name is misspelled (for example, `"secert"` instead of `"secret"`), the configuration fails to load with a JSON decoder error. Double-check field names against the auth types table above.

#### Dynamic registration fails

If remote servers cannot register dynamically:

1. Verify `accept_registrations` is set to `true` in the `mcp` section of your configuration (this field is not available in the `connectors` section).
2. Make sure the remote server sends a valid POST request to `/v1/mcp/register` with a JSON body containing `{"url": "<mcp-server-url>"}`.
3. The remote server must keep the HTTP connection open after registration. If the connection closes, Aperture unregisters the server's tools immediately.

### For users authorizing and using connectors

These issues affect authorizing a connector and calling its tools. You do not need access to the Aperture host. The **Connectors** page is enough to diagnose and recover.

#### A connector shows "Needs auth" or its tools are missing

Connectors that use `oauth2_authorization_code` require each user to authorize individually. Open the **Connectors** page, select the connector, and choose **Connect** to complete the consent flow. The connector's tools appear after you authorize.

#### Tools stop working after previously working

If tools from a connector using `oauth2_authorization_code` stop working after a period of inactivity, the refresh token has likely expired or been revoked by the provider. To recover, open the **Connectors** page, select the connector, and choose **Disconnect** and then **Connect** to re-authorize.

Disconnecting and reconnecting is safe and forces Aperture to generate fresh tokens. If you suspect your authorization is stuck (a token that is broken but still present), disconnect and reconnect to clear it.

#### Tool calls time out

If tool calls fail with timeout errors, the remote MCP server is not responding quickly enough. Aperture retries a tool call once on connection errors. On failure, it evicts the current session, reconnects to the remote server, and retries. If timeouts persist, ask your admin to check the remote server's performance and logs.

[ar-connect-flow]: #initiating-the-flow

[ar-dcr]: #dynamic-client-registration-oauth2_dcr

[ar-lazy-population]: #lazy-population

[ar-token-lifecycle]: #token-lifecycle

[ar-validation-errors]: #validation-errors

[kb-aperture-configuration]: /docs/aperture/configuration

[kb-aperture-grants]: /docs/aperture/configuration#grants

[xt-mcp-spec]: https://modelcontextprotocol.io/specification

[xt-rfc-7591]: https://www.rfc-editor.org/rfc/rfc7591

[xt-rfc-8414]: https://www.rfc-editor.org/rfc/rfc8414

[xt-rfc-9728]: https://www.rfc-editor.org/rfc/rfc9728
