Aperture connectors reference
Aperture connectors are in public alpha. The connectors grant syntax and the connectors configuration section may change. The mcp_tools, mcp_resources, and mcp_templates grant fields are deprecated. Use the connectors field instead.
The connectors section of the Aperture configuration configures outbound integrations with external services. Connectors support two protocols:
- MCP (
"protocol": "mcp"): Aperture connects to remote Model Context Protocol servers, aggregates their tools and resources, and exposes them through the/v1/mcpendpoint. - 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.
Connector labels are also always available. The previous connector_labels feature flag is deprecated and has no effect. Removing that gate cannot narrow anyone's access, because label grants only ever add access.
connectors fields
The connectors section accepts the following top-level fields:
| 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). |
system_labels | map | {} | Map of system connector ID to a list of labels, replacing that connector's default labels. Keys must name a known system connector (currently aperture); any other key is a load-time error. Refer to connector labels. |
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 displayed in the Aperture UI. For granted HTTP connectors, aperture_list_connectors also returns it to permitted users. When empty, the field is omitted from the stored configuration. |
context | string or JSON | No | N/A | Prompt or context information for a connector. Accepts a plain string or arbitrary JSON object. For granted HTTP connectors, aperture_list_connectors returns it to permitted users. |
labels | array of strings | No | [] | Flat labels that group the connector for access control. A grant pattern of label:<name> matches every connector carrying that label. Each label must match [a-zA-Z0-9][a-zA-Z0-9._-]*. The label system is reserved. Refer to connector labels. |
auth | object | No | N/A | Authentication configuration. Dispatched by the type field. |
Example with both protocols:
{
"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).
Connector labels
Labels are flat strings that group connectors for access control. Set the labels field on any connector, then grant access to the group with a label:<name> pattern in the connectors field of a grant:
{
"connectors": {
"servers": {
"salesforce": {
"protocol": "mcp",
"url": "https://api.salesforce.com/platform/mcp/v1/platform/sobject-all",
"labels": ["sales", "restricted"]
}
}
}
}
Each label must match [a-zA-Z0-9][a-zA-Z0-9._-]*. A label starts with a letter or digit and contains only letters, digits, dots, underscores, and hyphens. Slashes are rejected to avoid ambiguity with slash-separated FQN patterns. Because dots and hyphens are legal, labels can be namespaced (team.eng), but Aperture neither requires nor interprets that convention.
Beyond that grammar, labels are free text. Aperture defines no vocabulary and attaches no meaning to a label's value. The dashboard suggests labels already in use on other connectors so an Aperture gateway converges on one vocabulary rather than accumulating near-duplicates. Common schemes classify a connector by data sensitivity (public, private, restricted), by team ownership (eng, sales), or by provenance (system).
Label matching has two characteristics that differ from FQN patterns:
- Exact match, no globs. A
label:<name>pattern is compared literally against a connector's labels, the same waytag:andgroup:behave in Tailscale grants. Wildcard forms such aslabel:team.*match nothing. - Whole-connector access. A matching label grants every category and resource the connector exposes, including HTTP
proxyaccess. Labels have no per-tool granularity. For tool-level or resource-level access, use an FQN pattern that names the connector ID.
Labels widen access and never restrict it. Grants are additive and allow-only, so labeling a connector restricted documents intent without enforcing it: a user holding {"connectors": ["**"]} still reaches that connector. To limit a sensitive connector, remove the broad wildcard grants that reach it and grant it narrowly by label or by connector ID.
The reserved system label
Aperture applies the system label to the built-in aperture system connector automatically. The shipped default configuration grants label:system, so this connector works on a new Aperture gateway without configuration. Configured tailnet and tailnet_ssh connectors do not automatically receive the label. Because the default grant reaches every user, system is reserved under connectors.servers:
- The dashboard and the connector API reject
systemwhen you save a connector. - A configuration that already carries the label loads with a warning, and Aperture strips the label before evaluating any grant. An upgraded gateway still boots, and the label never reaches grant evaluation.
The label remains valid under connectors.system_labels, which exists to assign labels to built-in connectors. system_labels replaces a built-in connector's default labels rather than adding to them, so omitting system from the list stops the default label:system grant from matching that connector:
{
"connectors": {
"system_labels": {
"aperture": ["system", "public"]
}
}
}
For step-by-step instructions, refer to Grant connector access by label.
System connectors
Aperture includes built-in system connectors that appear independently of the connectors.servers configuration. System connectors appear in the GET /api/connectors response with category: "system".
The aperture connector's list_connectors tool is registered only when at least one HTTP connector is configured. With no HTTP connectors, the connector still appears in the listing, but the tool is absent from tools/list.
| ID | Provider | Protocol | Tools | Labels | Description |
|---|---|---|---|---|---|
aperture | Aperture | MCP | list_connectors | system | 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.
The system label states provenance only. Aperture ships no sensitivity labels on built-in connectors, because how sensitive a connector is depends on the deployment rather than on the connector. Override the defaults with connectors.system_labels.
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 fields that apply to its authentication type. Authorization-code entries can include auth_url, token_url, scopes, and auth_params. Dynamic client registration entries do not define authorization or token URLs. The following row illustrates an authorization-code 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. |
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. 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
{
"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). 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:
{"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"}. Withoutaccess_type: offline, Google does not issue a refresh token.
Google Workspace connectors currently require an OAuth client (Auth Client) created in a Google Cloud project enrolled in Google's Workspace Developer Preview Program. Credentials from a project that is not enrolled fail to authorize. This requirement is temporary and will be removed once Google makes Workspace MCP support generally available.
{
"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). 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 protected-resource metadata, then 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, token lifecycle, and 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.
{
"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
{
"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
{
"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
searchon thedocsconnector becomesdocs_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 updates
Aperture periodically checks configured remote MCP servers for capability changes. When a server adds or removes tools or resources, Aperture updates the capabilities available to connected clients.
If a remote server becomes unavailable, its tools and resources remain unavailable until the server recovers. Aperture makes them available again after recovery.
Connectors using oauth2_authorization_code authentication load capabilities for each user when they start a session. Users can request current capabilities with PUT /api/connectors/<id>/capabilities. This request contacts the upstream server and can return connection errors.
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 "label:system", which matches the built-in aperture connector, or an FQN pattern such as "aperture/**" or "aperture/tools/*".
The aperture system connector always appears in GET /api/connectors. Its aperture_list_connectors MCP tool is available 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 require grants like custom and verified connectors. Without a matching connectors grant, users cannot access the aperture connector's tools. The default configuration grants label:system to every user. Removing that grant makes the built-in tools unavailable.
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.
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, preferably read-only. Do not put sensitive information in description or context. The aperture_list_connectors tool returns these fields to users who can call it.
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
AuthorizationandCookieheaders 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'sauthconfiguration. - Response headers: Aperture strips
Set-Cookieheaders 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
authconfiguration. Client-suppliedAuthorizationheaders are always replaced.
Dynamic registration
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.
{
"mcp": {
"accept_registrations": true,
"servers": {}
}
}
Remote servers register by sending a POST request to /v1/mcp/register with a JSON body containing their URL:
curl -X POST http://<aperture-hostname>/v1/mcp/register \
-H "Content-Type: application/json" \
-d '{"url": "http://my-mcp-server:8080/v1/mcp"}'
Aperture validates the registering server before it responds with HTTP 200. Each dynamically registered server receives a sequential ID such as auto1 or auto2, and Aperture prefixes its tools accordingly, for example auto1_search.
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. 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, or with label:<name> patterns that match by label. The following example grants all users the built-in Aperture tools through the system label, all tools from the docs connector, and proxy access to the github HTTP connector:
{
"grants": [
{
"src": ["*"],
"app": {
"tailscale.com/cap/aperture": [
{"connectors": ["label:system"]},
{"connectors": ["docs/tools/*"]},
{"connectors": ["github/**"]}
]
}
}
]
}
The connectors field accepts an array of FQN glob strings and label:<name> strings. Each FQN 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": thesearchtool from thedocsconnector."docs/tools/*": all tools from thedocsconnector."docs/**": all capabilities from thedocsconnector (tools, resources, templates)."github/proxy": proxy access to thegithubHTTP connector."github/**": all access togithub(proxy and any future categories)."**": all capabilities from all connectors."label:system": every connector labeledsystem. By default, this is the built-inaperturesystem connector. This pattern is in the shipped default configuration."label:team.eng": every connector labeledteam.eng, at every category and resource.
A top-level pattern like "connectorID/*" is automatically expanded to "connectorID/**", so it covers all categories and sub-resources.
A label:<name> pattern is matched exactly rather than expanded as a glob, and a match grants the whole connector. Refer to connector labels for the label grammar and matching rules.
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.
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 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:
"mcp": { "servers": { "docs": { "url": "http://mcp-server.example.ts.net:8185/v1/mcp" } } }
The equivalent connectors entry is:
"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 |
| Invalid connector label | connectors.servers.{id}.labels: invalid label "{label}"; labels must match [a-zA-Z0-9][a-zA-Z0-9._-]* (no slashes) | Error |
| Reserved label on a user connector | connectors.servers.{id}.labels: removed reserved label "system" | Warning |
Unknown system connector in system_labels | connectors.system_labels.{id}: unknown system connector ID | 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 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:
{
// 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, grouped by label
"snowflake": {
"protocol": "mcp",
"url": "https://mcp.example.com/v1/mcp",
"labels": ["analytics", "restricted"],
"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 the built-in Aperture tools (the shipped default) and,
// separately, everything the analytics team needs by label
"grants": [
{
"src": ["*"],
"app": {
"tailscale.com/cap/aperture": [
{"connectors": ["label:system"]}
]
}
},
{
"src": ["group:analytics"],
"app": {
"tailscale.com/cap/aperture": [
{"connectors": ["label:analytics"]}
]
}
}
]
}
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:
- 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.
- Verify the URL in your configuration is correct and the MCP server is running.
- Verify your grants include
connectorspatterns 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:
- Verify
accept_registrationsis set totruein themcpsection of your configuration (this field is not available in theconnectorssection). - Make sure the remote server sends a valid POST request to
/v1/mcp/registerwith a JSON body containing{"url": "<mcp-server-url>"}. - 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.