Skip to content

Connectors Catalog

The Settings → Connectors Browse list is the catalog: one directory of ServerDetail files — the shape the upstream MCP registry publishes — read, validated, and projected into the rows a workspace can install from.

Only an entry advertising a remotes[] endpoint is installable: the runtime connects to remote MCP servers and does not acquire, verify, or execute downloadable packages[]. An entry offering only packages is dropped from Browse with a logged reason.

NB_CURATED_CATALOG_DIR names a directory. Every *.yaml, *.yml, and *.json in it is read in sorted filename order and aggregated into one catalog; unset, the runtime falls back to the minimal DCR-only example baked into the image so a fresh install is not empty.

Splitting curation across files is the intended shape, not a workaround — curated.yaml, composio.yaml, and one per gateway keep unrelated curation in separate reviewable files that still roll up to a single list. Each file is validated on its own, so a diagnostic names the file it came from, and a file that cannot be read or parsed is reported without emptying the rest of the catalog. Two files carrying the same server id resolve first-file-wins.

The directory is re-read on every lookup, so editing a mounted ConfigMap takes effect without restarting the pod.

Add the entry to your deployment’s catalog directory — the one NB_CURATED_CATALOG_DIR points at. (src/connectors/catalog/curated/example.yaml is the in-image example: DCR-only, handy for local dev, and replaced wholesale in any real deployment.) Either way the file is a list of upstream ServerDetail records; platform-specific fields live under the reverse-DNS extension key _meta["ai.nimblebrain/connector"].

The kinds and what each asks of a user are documented once, in Connectors → the auth kinds. What matters when authoring an entry is which to reach for:

  • Prefer dcr. If the vendor exposes /.well-known/oauth-authorization-server with registration_endpoint set, use it — zero operator setup, no third party in the trust path. (Granola, Notion, Linear, Stripe.) Verify the claim before shipping the entry with the rot detector.
  • Use static when the vendor requires a pre-registered OAuth app. Costs a workspace-admin setup step, per workspace. (Asana, HubSpot, Zoom.)
  • Use a brokered kind when the vendor’s API needs app verification you don’t hold — composio for OAuth aggregation (Gmail, Outlook), smithery for a server already published to the Smithery registry, which brokers the connection and hosts the session. Either costs a platform-wide provider configuration.
  • provider names a credential provider that supplies the transport credential server-side, with no user or operator OAuth. Two things use it: the platform’s own connectors, whose credential the runtime mints against the fleet authorizer, and a gateway you hold one account key for — see gateway endpoints below.
servers:
- name: app.linear/mcp
title: Linear
description: Issues, projects, and product roadmaps
version: "1.0.0"
icons:
- src: https://static.nimblebrain.ai/icons/linear.png
sizes: ["any"]
remotes:
- type: streamable-http
url: https://mcp.linear.app/mcp
_meta:
ai.nimblebrain/connector:
defaultScope: workspace
auth: dcr
tags: [issues, project-mgmt]

For auth: composio, include a composio block with the toolkit slug and (strongly recommended) a curated tool allowlist:

servers:
- name: com.google/gmail
title: Gmail
description: Read, send, and draft mail
version: "1.0.0"
icons:
- src: https://static.nimblebrain.ai/icons/gmail.png
sizes: ["any"]
remotes:
- type: streamable-http
url: https://backend.composio.dev/v3/mcp
_meta:
ai.nimblebrain/connector:
defaultScope: workspace
auth: composio
composio:
toolkit: gmail
tools:
- GMAIL_SEND_EMAIL
- GMAIL_FETCH_EMAILS
- GMAIL_CREATE_EMAIL_DRAFT
# ... 10-15 total
tags: [mail, productivity, composio]

The url is a fixed placeholder; the actual session URL is minted per install from Composio’s session API. composio.tools is an allowlist of upstream tool names — without it the connector exposes every tool the toolkit publishes (Outlook ships ~280, enough to blow past Claude’s input-token budget on first turn). Curate to a working set.

For auth: smithery, include a smithery block naming the registry qualified name:

servers:
- name: ai.bassethound/mcp
title: Bassethound
description: Company intelligence for AI agents
version: "1.0.0"
remotes:
- type: streamable-http
url: https://mcp.bassethound.ai/mcp
_meta:
ai.nimblebrain/connector:
auth: smithery
smithery:
server: nimblebrain/bassethound
tags: [intelligence, research]

The url is a placeholder — the install replaces it with the brokered session URL. A Smithery qualified name is global, so the entry is complete on its own and only the platform-wide SMITHERY_API_KEY and the declared connectors.providers.smithery.namespace are deployment state. There is no tool allowlist either; the connection exposes the server’s own surface.

For auth: static, also include operatorSetup:

servers:
- name: io.asana/mcp
title: Asana
description: Tasks, projects, and team workflows
version: "1.0.0"
icons:
- src: https://static.nimblebrain.ai/icons/asana.png
sizes: ["any"]
remotes:
- type: streamable-http
url: https://mcp.asana.com/v2/mcp
_meta:
ai.nimblebrain/connector:
defaultScope: workspace
auth: static
operatorSetup:
portalUrl: https://app.asana.com/0/my-apps
hint: Create an OAuth app in Asana developer portal, copy client_id + client_secret
clientSecretKey: asana.client_secret
tags: [tasks, projects]

Top-level fields come from upstream ServerDetail:

Field Required Description
name yes Reverse-DNS canonical id (e.g. io.asana/mcp). Must match ^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$. Used as the entry’s primary key throughout the platform.
title no Display name on the Browse card. Falls back to name when absent.
description yes One-line tagline. ~80 chars.
version yes Semver string.
icons[].src yes Absolute http(s) URL. javascript: / data: / file: are rejected at the directory boundary.
remotes[] for OAuth connectors List of remote transports. First entry drives the install. type is streamable-http or sse.
packages[] never Downloadable distributions. Read for display and scope-matching only — this runtime installs no code, so an entry offering only packages[] is not installable and is dropped from Browse.

NimbleBrain-specific fields under _meta["ai.nimblebrain/connector"]:

Field Required Description
defaultScope recommended "workspace" (shared identity) or "user" (per-user account). Defaults to workspace. auth: composio is workspace-scope only, and auth: smithery installs into a workspace only.
auth required for remotes[] "dcr", "static" or "provider" (the runtime-native kinds), or the id of a configured brokered provider — "composio" / "smithery" today. See auth kinds. Defaults to dcr. A brokered entry carries that provider’s config under a key of the same name (composio:, smithery:), which is how the platform hands a provider its own coordinates without knowing their shape.
requiredScopes no OAuth scopes the connection requests. When set, they are exactly the scopes on the authorize request, even where the server advertises more — use it to hold a connector to a narrower grant (read-only, no deletes). Pinning offline_access also asks for consent. A dcr client is still registered with the scopes the server advertises, so pin only scopes from that set against a server that enforces registered scope. The pin also blocks step-up: a tool the server refuses for insufficient scope fails, and on a connection holding no refresh token it sends the connector to reauthorization, where reconnecting grants the same pin. Omit it to request the scopes the server names in its challenge or advertises; a server that names none gets no scope at all.
additionalAuthorizationParams no Extra authorize-URL params (e.g. Google’s access_type: offline). Reserved keys (client_id, state, redirect_uri, response_type, code_challenge, code_challenge_method, scope, etc.) are rejected.
operatorSetup required when auth: static { portalUrl, hint, clientSecretKey }.
secretHeaders no, auth: provider only Header name → { ref: credential, key, label?, help? }, for a connection that must present the workspace’s own secret as well as its own credential. Resolved on every request at that workspace’s scope. Only references are accepted — a literal is refused at install, naming the header. The install dialog asks the user for each value, using label / help when the entry sets them and a label derived from the key’s last segment otherwise; both are display-only and never reach the transport. Nothing carries the field on any other auth kind, where it is silently ignored. See per-workspace credentials.
composio required when auth: composio { toolkit, tools?, authScheme?, fields? }. toolkit is Composio’s slug for the upstream, and doubles as the key your deployment’s connectors.providers.composio.authConfigs is looked up under — the catalog itself carries no deployment-specific ids. tools is an optional allowlist of upstream tool names — strongly recommended to keep agent context bounded. authScheme defaults to OAUTH2 (the redirect flow); set it to API_KEY for toolkits that authenticate by key (see below). fields is required for API_KEY — the inputs collected from the user at connect.
account no { tool, arguments?, field }. How to ask the service which account a connection is signed in as, when its sign-in does not say. See Showing the connected account.
tags no Strings used for filter / search in the Browse page.
interactive no Sets the “Interactive” badge.
docsUrl no Connector-specific docs link surfaced on the Configure page. Must be http(s).

A gateway such as MCP360 publishes MCP endpoints and issues one account-wide API key. There is no per-connection API to call, so nothing is brokered: the entry names the endpoint, and a credential provider attaches the key.

servers:
- name: ai.example-gateway/search
title: Example Gateway — Search
description: What this endpoint gives the agent
version: "1.0.0"
remotes:
- type: streamable-http
url: https://gateway.example.com/v1/search/mcp
_meta:
ai.nimblebrain/connector:
auth: provider
providerAuth:
provider: example-gateway
config: {}
tags: [search]

providerAuth is copied verbatim into the connector’s transport at install. The install path re-reads the entry from the operator-published catalog first and refuses a provider-auth install it cannot verify there, so a workspace admin cannot forge an entry that spends your account key against an endpoint of their choosing.

The provider name must match one registered on the platform. Registering one is a small addition beside the built-in credential providers; src/connectors/providers/smithery/transport-credential.ts is the closest model.

Every workspace installing such an entry shares the one key, so the gateway sees a single caller. You get no per-workspace attribution and no independent revocation. Where a vendor offers a brokered path as well, prefer it.

Some Composio toolkits authenticate by API key rather than OAuth. They skip the redirect: the user pastes the key (and any region/host) into a form, the platform hands it to Composio at connect time, and only an opaque account pointer is persisted — never the key.

Set authScheme: API_KEY and declare the fields to collect:

_meta:
ai.nimblebrain/connector:
auth: composio
composio:
toolkit: posthog
authScheme: API_KEY # default is OAUTH2 (the redirect flow)
fields:
- key: generic_api_key # MUST match Composio's connection field name
title: Personal API Key
sensitive: true # rendered as a password input
required: true # required unless explicitly false
- key: subdomain
title: Region / subdomain
required: true
tools:
- POSTHOG_CREATE_QUERY_IN_PROJECT_BY_ID
# ...curated allowlist

Each fields[].key is sent to Composio verbatim, so it must match the toolkit’s connection-initiation field name — which is often not what the REST tool catalog suggests (e.g. PostHog’s key field is generic_api_key, not api_key). The same namespace caveat applies to tools slugs. The operator runbook for creating the API_KEY auth config and verifying field keys + tool slugs is in Composio Aggregator Setup → API-key toolkits.

Long-running agents and scheduled tasks need a connector to refresh its own access token. Most vendors return a refresh_token from the authorization-code exchange by default, or when you request the standard offline_access scope via requiredScopes. A few gate it behind a non-standard authorize-URL param — without it the vendor issues only a short-lived access token and no refresh token, so the connection dies at every access-token expiry and has to be reconnected by hand (a daily task then fails almost every run).

Set the param via additionalAuthorizationParams:

Vendor Param
Dropbox token_access_type: offline
Google (native OAuth) access_type: offline + prompt: consent
_meta:
ai.nimblebrain/connector:
auth: static
additionalAuthorizationParams:
token_access_type: offline
requiredScopes:
- files.metadata.read

The param is added to the authorize request only, so existing connections do not retroactively gain a refresh token — users must reconnect once after the catalog change ships.

A connected connector shows the account it is signed in as (“Connected as alice@example.com”), so nobody has to guess which of their accounts an agent is acting on. The account comes from whoever knows it:

  1. The authorization server, for an OIDC server. The runtime asks for openid and email and reads the id_token or the userinfo endpoint at sign-in. Nothing to configure, unless you pin requiredScopes: a pin is the whole request, so include openid and email in it.
  2. The broker, for auth: composio. The runtime reads the account name Composio records for some toolkits, or else the email in the id_token the vendor issued, when the connection lands. For a Google toolkit that means granting openid and https://www.googleapis.com/auth/userinfo.email on its auth config in Composio, not here.
  3. The service itself, for everything else: a server that is not OIDC, or a brokered connection with no account recorded. Declare the tool that answers for the signed-in user:
_meta:
ai.nimblebrain/connector:
auth: composio
composio:
toolkit: zoom
tools: [ZOOM_GET_USER, ZOOM_LIST_MEETINGS]
account:
tool: ZOOM_GET_USER # the connector's own tool, by its bare name
arguments: { userId: me } # optional
field: data.email # dotted path into the tool's JSON answer

The runtime calls that tool once, the first time the connected connector is listed with no account recorded, and keeps the answer with the connection until the next sign-in replaces it. It asks only a connection that has signed in and is live in the runtime, and only a connector installed from this entry (same URL, or the same brokered id). The listing waits at most five seconds for the answer.

  • tool must be one the connection can call: for auth: composio it has to be in composio.tools, and its scope has to be in the grant.
  • field is a path through objects in the answer (structuredContent, or the first text block read as JSON). It must end at a short single-line string. Anything else shows no account, and the runtime logs account lookup for <connector> named no account with which half failed.
  • Pick a tool that only reads. The call runs with the connection’s own credential, outside any agent run and outside the tool permissions a person set for agents.

The vendor’s OAuth app must allow this exact callback URL:

{NB_PUBLIC_ORIGIN}/v1/mcp-auth/callback

Where NB_PUBLIC_ORIGIN is the canonical public origin of the deployment. For the dev server it defaults to http://localhost:27247. In production this must be set explicitly (or derived from the chart-forwarded host facts) — see Environment Variables → Public origin.

The Set up modal in the UI shows the resolved redirect URI with a Copy button so admins can register it without guessing.

Terminal window
bun run dev

The new entry appears on Settings → Connectors → Browse. Walk through the install:

  1. Click Install on the new connector.
  2. (For static-auth) Click Set up in the workspace admin modal, paste client_id + client_secret, save.
  3. Click Connect, complete the vendor’s consent flow.
  4. Open the connector detail page. The status pill should read Ready and the tools list populates.

If anything is off, check the API server logs for [static-source] / [connector-directory] warnings — invalid entries are dropped with a logged reason.

Each file in the catalog directory is a YAML or JSON document with one of these shapes:

servers:
- name: io.example/mcp
description: ...
# ... full ServerDetail

Or a bare JSON array for minimal override files:

[{ "name": "io.example/mcp", "description": "...", "version": "1.0.0" }]

Every entry is validated against the upstream ServerDetail JSON schema. Invalid entries are dropped with a logged warning naming the file and the entry name; the rest of the file still loads.

apiVersion: v1
kind: ConfigMap
metadata:
name: nimblebrain-connectors-catalog
namespace: nimblebrain
data:
acme-catalog.yaml: |
servers:
- name: io.acme.support/mcp
title: Acme Support
description: Internal support tooling
version: "1.0.0"
icons:
- src: https://static.acme.example/icons/support.png
remotes:
- type: streamable-http
url: https://mcp.acme.example/support
_meta:
ai.nimblebrain/connector:
defaultScope: workspace
auth: dcr
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nimblebrain
spec:
template:
spec:
containers:
- name: api
env:
- name: NB_CURATED_CATALOG_DIR
value: /etc/nimblebrain/connectors
volumeMounts:
- name: connectors-catalog
mountPath: /etc/nimblebrain/connectors
readOnly: true
volumes:
- name: connectors-catalog
configMap:
name: nimblebrain-connectors-catalog

Mount as many keys into that directory as curation needs — each becomes one catalog file. Because the directory is re-read on every lookup, updating the ConfigMap changes Browse without restarting the pod.

Every ServerDetail is validated when its file is read and again when the catalog projects it. Invalid entries are dropped with a logged warning naming the file and the entry name. Common rejection reasons:

Reason Fix
name must match pattern "^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$" Use the reverse-DNS form (io.asana/mcp), not a bare slug.
icon src must be http(s) Drop data: / javascript: / file: schemes.
auth='static' requires operatorSetup.{portalUrl,hint,clientSecretKey} Add the operatorSetup block under _meta["ai.nimblebrain/connector"].
additionalAuthorizationParams cannot include reserved keys Remove client_id, redirect_uri, state, code_challenge, code_challenge_method, scope, etc. — these are filled by the OAuth provider.
duplicate name "<id>" Each entry’s name must be unique within a single static source.
refused — its server name "<slug>" is also the server name of "<id>" Two names slugify to one server name (a.b/c and a/b.c are both a-b-c), so neither loads. Rename one. Logged as an error.
not installable: needs a \remotes` entry` The entry advertises only packages[]. This runtime connects to remote MCP servers; give the entry a remotes[] URL.

An entry’s grants — its host UI, hooks delivery and its outbox — apply only to an installed connector at the entry’s remotes[0].url (a brokered connector: the provider and entry its broker recorded); a connector that only shares the entry’s server name gets none of them. Its restriction, admin_tools, applies to every connector under its server name. Changing an entry’s URL therefore takes its grants from every existing install until it is reinstalled.

When the top-level shape is bad (not a list or {servers: [...]}, malformed YAML), the whole override is rejected and the bundled catalog is used. The startup logs name the file path and the parse error.

A rejected entry is dropped, not fatal — its siblings load normally, so the only symptom is a connector that never appears in Browse. To find that before it reaches a running deployment, point the checker at any ServerDetail file or directory:

Terminal window
bun run scripts/check-catalog-schema.ts <catalog-file-or-dir>

It reports every entry that would be dropped — by the source’s schema and dedup checks, by the directory boundary’s safety scrub, or by being unresolvable to anything installable — and exits non-zero if there are any. A source-stage diagnostic is worded exactly as the runtime logs it, so it doubles as the string to grep for when confirming a fix landed; the directory-boundary ones name the entry and the reason but are not log-line copies.

✗ connectors/: 1 problem(s) — these entries would be dropped
connectors/platform.yaml[4:io.example/mcp] dropped — invalid ServerDetail: /description must NOT have more than 100 characters

Offline and deterministic, unlike the DCR probe below, so it is cheap enough to gate every catalog change. bun run check:catalog-schema runs it over the catalog bundled with the image as part of bun run verify; a deployment whose catalog lives in NB_CURATED_CATALOG_DIR should run it against that directory in the repo that owns it.

The platform ships a network-dependent probe that verifies every auth: dcr entry in a catalog actually serves the OAuth discovery chain it claims. Point it at any ServerDetail file or directory:

Terminal window
bun run scripts/check-catalog-dcr.ts <catalog-file-or-dir> <redirect-uri>

<redirect-uri> is the https MCP OAuth callback your deployment registers with vendors, such as https://nb.example.com/v1/mcp-auth/callback. It is required because vendors check it against their own redirect-host allowlists, so only the real callback gives a truthful result.

Steps 1–4 are the discovery chain the production OAuth provider uses; steps 5 and 6 then exercise the flow for real against the vendor.

  1. Reachability — HEAD <remotes[0].url>.
  2. RFC 9728 — GET <connector-origin>/.well-known/oauth-protected-resource to discover the authorization server origin.
  3. RFC 8414 — GET <as-origin>/.well-known/oauth-authorization-server against each candidate AS origin (RFC 9728 advertised, plus the connector origin as fallback).
  4. RFC 7591 — the AS metadata document must include registration_endpoint.
  5. DCR registration — POST a synthetic client to the registration_endpoint with that redirect URI. Catches vendors that advertise DCR but enforce a redirect-URI host allowlist (Intercom, Vercel).
  6. Authorize — GET the authorization_endpoint with that client_id and the same redirect URI. Catches vendors that accept the registration but reject the redirect URI one step later (Canva).

An entry passes only if every step succeeds; step 6 is skipped when the AS advertises no authorization_endpoint. Steps 5 and 6 are what catch “DCR theater” — a vendor that ships the RFC 7591 endpoints but enforces a parallel host allowlist the spec is meant to make obsolete, which discovery alone reports as healthy.

In this repo it is a manual CLI — deliberately, since the catalog it needs to check is deployment config living in the directory NB_CURATED_CATALOG_DIR points at, not in the image. The automated gate runs in the deploy repo alongside that real catalog, so a vendor going down or dropping DCR support breaks the catalog’s own PR. It is not part of bun run verify, which stays offline.

The catalog never contains secrets. For auth: static entries, the workspace admin sets client_id (which lives in workspace.json under oauthOperatorApps[<id>].clientId) and client_secret (which lives in the workspace credential store under the key declared by operatorSetup.clientSecretKey).

Both are keyed by the connector’s reverse-DNS name (e.g. io.asana/mcp), not a short slug.

Both are set through the UI:

Settings → Connectors → <connector> → Set up

The modal writes the public client_id into workspace.json → oauthOperatorApps[<id>].clientId and stores the client_secret in the workspace credential store under operatorSetup.clientSecretKey. There is no flag-based headless command for an operator OAuth secret — use the Set up modal (or the manage_connectors tool action it calls).

Operators upgrading from a release before the ServerDetail alignment landed should be aware of these one-time migrations:

  • NB_CATALOG_PATH and NB_STDIO_CATALOG_PATH removed. Point NB_CURATED_CATALOG_DIR at a directory of ServerDetail YAML / JSON files.
  • Catalog id format. A catalog listing’s id (and InstalledConnector.catalogId) is now the reverse-DNS ServerDetail.name (io.asana/mcp) instead of a bare slug (asana). Catalog-keyed state — workspace.json#oauthOperatorApps[<id>] and the matching <clientSecretKey> credential entries — must be rekeyed to the reverse-DNS form before existing static-auth connectors will resolve. There’s no auto-migration; rekey workspace by workspace through the admin Connectors UI.
  • Connector serverName slug. A catalog install persists a slugified canonical reverse-DNS form (com-canva-mcp), and every connector entry carries its serverName.
  • packages[]-only entries are not installable. The runtime does not acquire or spawn a server’s code, so a catalog entry advertising only packages[] never appears in Browse. Publish the server behind a URL and give the entry a remotes[] endpoint.