Skip to content

Connectors Catalog

The Settings → Connectors Browse list is assembled by aggregating one or more registries into a single directory. Every registry produces the same wire shape — the upstream MCP registry’s ServerDetail — so curated remote OAuth services and mpak-published stdio bundles render side-by-side without separate plumbing.

A registry is a typed source of ServerDetail records. Each instance is identified by a stable id and a type:

TypeSourceWhen to use
staticA YAML / JSON file containing ServerDetail entriesCurated lists you ship with the platform or mount via ConfigMap.
mpakThe mpak HTTP registry (registry.mpak.dev by default)Discovering third-party stdio bundles published to mpak.
mcpAn upstream MCP /v1/servers endpointPointing at any spec-conformant MCP registry. (Discovery API only — no install path yet.)
custom-urlReservedFuture expansion.

The platform seeds two registries on first boot:

  • bundled-static (locked, type static) — the curated catalog directory: src/connectors/curated/ in-image, or wherever NB_CURATED_CATALOG_DIR points. Every *.yaml/*.yml/*.json in it loads into this one registry. Cannot be disabled or removed; operators may rename it.
  • mpak (type mpak, scoped to ["nimblebraininc"]) — the public mpak registry, narrowed to the @nimblebraininc/* scope so first-time installs see only NimbleBrain-curated bundles. Operators can broaden the scope or disable the registry entirely.

Registry config persists at <workDir>/registries.json. The shape is:

{
"registries": [
{
"id": "bundled-static",
"name": "Curated services",
"type": "static",
"enabled": true,
"locked": true,
"url": "/abs/path/to/catalog.yaml"
},
{
"id": "mpak",
"name": "mpak.dev",
"type": "mpak",
"enabled": true,
"scopes": ["nimblebraininc"]
}
]
}

The scopes field (when present) restricts a registry’s entries to one or more namespaces. A ServerDetail passes if any of these match any configured scope:

  • The reverse-DNS prefix of ServerDetail.name (ai.nimblebrain matches ai.nimblebrain/echo).
  • The npm scope of any packages[].identifier (nimblebraininc matches @nimblebraininc/echo).

Add the entry to your deployment’s catalog directory — the one NB_CURATED_CATALOG_DIR points at. (src/connectors/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 is for platform-managed connectors whose credential the runtime mints itself against the fleet authorizer. Not something you author for a third-party service.
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 (via Composio)
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:

FieldRequiredDescription
nameyesReverse-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.
titlenoDisplay name on the Browse card. Falls back to name when absent.
descriptionyesOne-line tagline. ~80 chars.
versionyesSemver string.
icons[].srcyesAbsolute http(s) URL. javascript: / data: / file: are rejected at the directory boundary.
remotes[]for OAuth connectorsList of remote transports. First entry drives the install. type is streamable-http or sse.
packages[]for stdio bundlesList of installable packages. identifier is the scoped npm name (e.g. @nimblebraininc/echo).

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

FieldRequiredDescription
defaultScoperecommended"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.
authrequired for remotes[]"dcr", "static", "composio", "smithery", or "provider" — see auth kinds. Defaults to dcr.
requiredScopesnoList of OAuth scope strings.
additionalAuthorizationParamsnoExtra 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.
operatorSetuprequired when auth: static{ portalUrl, hint, clientSecretKey }.
composiorequired 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.
tagsnoStrings used for filter / search in the Browse page.
interactivenoSets the “Interactive” badge.
docsUrlnoConnector-specific docs link surfaced on the Configure page. Must be http(s).

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 automations 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 automation then fails almost every run).

Set the param via additionalAuthorizationParams:

VendorParam
Dropboxtoken_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.

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.

Stdio bundles install via the mpak SDK, which fetches the published @nimblebraininc/<name> package. There is no separate stdio catalog file: any bundle published to a registered mpak registry whose packages[].identifier matches the registry’s scopes filter surfaces in Browse automatically.

To narrow or broaden which mpak bundles operators see, edit the seeded mpak row’s scopes list (default ["nimblebraininc"]):

{
"id": "mpak",
"type": "mpak",
"enabled": true,
"scopes": ["nimblebraininc", "acme-internal"]
}

Drop the scopes field entirely to surface every bundle on the registry — usually too noisy for first-time users.

Operators with custom curation needs can replace the persisted registries.json at runtime via the NB_REGISTRIES env var. Setting this env var completely overrides any stored registries.json for the lifetime of the process; the bundled-static entry is re-pinned automatically so a malformed override can’t accidentally drop the platform default.

Terminal window
export NB_REGISTRIES='[
{
"id": "acme-internal",
"name": "Acme Internal",
"type": "static",
"enabled": true,
"url": "/etc/nimblebrain/connectors/acme-catalog.yaml"
},
{
"id": "mpak",
"name": "mpak.dev",
"type": "mpak",
"enabled": true,
"scopes": ["nimblebraininc", "acme"]
}
]'

Each entry must include id, name, and type. Supported types: static, mpak, mcp, custom-url. Optional fields: enabled (default true), url, scopes, locked.

When NB_REGISTRIES is set, persistent edits via the admin UI are rejected — the env var is the source of truth. Unset it to fall back to the persisted file.

The file referenced by a static registry 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 source path and the entry name.

apiVersion: v1
kind: ConfigMap
metadata:
name: nimblebrain-registries
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_REGISTRIES
value: |
[
{
"id": "acme-internal",
"name": "Acme Internal",
"type": "static",
"enabled": true,
"url": "/etc/nimblebrain/registries/acme-catalog.yaml"
}
]
volumeMounts:
- name: registries
mountPath: /etc/nimblebrain/registries
readOnly: true
volumes:
- name: registries
configMap:
name: nimblebrain-registries

The platform reads NB_REGISTRIES once at process start. Edits to the env var (or the mounted file) require a pod restart to take effect.

Every ServerDetail is validated at the source boundary (mpak fetch, static-source file read) and again at the directory boundary. Invalid entries are dropped with a logged warning naming the registry id and the entry name. Common rejection reasons:

ReasonFix
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 keysRemove 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.

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>

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. ReachabilityHEAD <remotes[0].url>.
  2. RFC 9728GET <bundle-origin>/.well-known/oauth-protected-resource to discover the authorization server origin.
  3. RFC 8414GET <as-origin>/.well-known/oauth-authorization-server against each candidate AS origin (RFC 9728 advertised, plus bundle 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 a representative redirect URI. Catches vendors that advertise DCR but enforce a redirect-URI host allowlist (Intercom, Vercel).
  6. AuthorizeGET 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:

  • RegistryType rename. "curated""static" and "directory""mcp". Any NB_REGISTRIES JSON pinning the old strings must be re-typed. The persisted registries.json is auto-migrated on next read by re-seeding the locked default.
  • Seeded id rename. The locked seeded registry id "curated" is now "bundled-static". Existing registries.json rows are preserved; the new id is re-added if missing.
  • NB_CATALOG_PATH and NB_STDIO_CATALOG_PATH removed. Use NB_REGISTRIES with a static-type registry pointing at a ServerDetail[] YAML / JSON file.
  • Catalog id format. DirectoryEntry.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.
  • stdio-catalog.yaml removed. Curated stdio bundle entries no longer live in a separate file — the mpak registry is queried directly via the SDK and filtered by scopes.
  • Bundle serverName slug. New catalog installs persist a slugified canonical reverse-DNS form (com-canva-mcp, dev-mpak-nimblebraininc-echo) instead of a short brand slug. Existing installs keep their persisted serverName via a fallback. Operators querying by serverName (logs, audit) need both forms during the transition.