Cosmo Docs
Server

Connectors Module

MCP connector instances, credentials, and provider registry

The Connectors module manages per-org MCP integration instances and the global connector provider catalog. Source: packages/server/src/modules/connectors/ and packages/server/src/modules/connector-providers/.

Connectors REST Endpoints

All endpoints are prefixed with /api/v1/connectors and require ClerkAuthGuard.

MethodPathDescription
GET/connectorsList org's connector instances
POST/connectorsCreate a connector instance
PATCH/connectors/:instanceIdRename a connector's account label
GET/connectors/:instanceId/credentialsGet credentials for an instance
POST/connectors/:instanceId/credentialsUpdate credentials
DELETE/connectors/:instanceIdDelete a connector instance

Connector Providers REST Endpoints

MethodPathGuardsDescription
GET/connector-providersNoneList enabled providers (public)
PUT/connector-providersClerkAuthGuardCreate or update a provider (admin)
DELETE/connector-providers/:serverIdClerkAuthGuardDelete a provider (admin)

Connectors Service (services/connectors.service.ts)

list(orgId, userId)

Returns only the connector instances owned by the requesting user. userId is required — every connector belongs to exactly one user (COS-739), so there is no org-wide view and no way to list another member's connections.

There used to be an org-shared mode where userId was null and one credential served the whole org. It was removed: one member disconnecting took the connector away from everyone, and members could read the admin's tokens.

Hosting override logic:

  • REMOTE_OAUTH and NONE connectors: hosting is set to LOCAL in the response (the desktop manages these locally).
  • All others: hosting is CLOUD (server manages credentials).

create(orgId, userId, dto)

Creates a new MCPConfig record. Input:

FieldRequiredDescription
serverIdYesProvider ID (e.g. "notion")
instanceIdYesClient-generated unique ID
labelYesHuman-readable name
hostingNoCLOUD / LOCAL
transportNoSTDIO / SSE / HTTP
configNoNon-sensitive config JSON
credentialsNoInitial credentials JSON

userId is always stored on the record, for every provider — the caller becomes the owner. perUserAuth no longer affects scoping; it only describes where the provider's tokens live.

Uniqueness constraint: (orgId, userId, instanceId).

serverId must resolve to a real provider — seeded (ConnectorProvider) or one of this org's own custom servers (CustomConnectorProvider). A serverId that matches neither is refused with a 400 (Unknown connector provider "...") rather than creating a connector row for a provider that can never actually connect (QA follow-up on COS-739).

The desktop client proves the connector actually works before this endpoint is ever called. ConnectorManager.addInstance opens a real MCP client connection to the endpoint and closes it again — on the API_KEY branch with the supplied key (QA follow-up on COS-739), and on the NONE branch with no credentials (COS-979). Either way a connector that cannot serve MCP is refused at connect time rather than being stored, marked CONNECTED, and only proving broken the first time the chat engine reaches for its tools.

Because this is a real MCP initialize handshake rather than a reachability ping, an ordinary web page answering 200 fails it. That is the point: the server-side validation in custom-provider-validation.ts can only check what a URL looks like (https, public host, no embedded credentials), which any arbitrary address satisfies.

This is client-side by necessity — the server only ever receives credentials it is handed, and has no way to dial an arbitrary custom MCP endpoint itself. See probeMcpConnection in packages/desktop/src/main/connector-manager.ts.

REMOTE_OAUTH needs no separate probe: runRemoteOAuthFlow already performs its own Client.connect() against the same endpoint, so a URL that is not an MCP server fails there. STDIO connectors are exempt too — they run a local child process and have no endpoint to dial (isRemoteProbeable).

getCredentials(orgId, instanceId, userId?)

Fetches the credentials JSON for a connector. For per-user connectors, userId must match the record's user.

updateCredentials(orgId, instanceId, credentials, userId)

Replaces the credentials JSON and updates status to CONNECTED and lastUsedAt to now.

rename(orgId, instanceId, label, userId)

Updates only the account label on the caller's own connector — the text that distinguishes two instances of the same provider in the Integrations list. Scoped through the same owner-only lookup as every other write (COS-739 QA follow-up: there was previously no way to fix a typo in a connector's label without disconnecting and reconnecting it). label is validated with the same app-wide name bound as create's label field.

Connector Providers Service (services/connector-providers.service.ts)

listPublic()

Returns all providers where enabled: true, with sensitive fields stripped:

  • clientSecret is not included.
  • authParams with secrets are excluded.

Returned fields: serverId, displayName, description, iconUrl, category, authMode, perUserAuth, requiresAdminConsent, mcpTransport.

getOAuthConfig(serverId)

Internal method (not exposed via REST). Returns the full provider record including clientId, clientSecret, authUrl, tokenUrl, scopes. Used by MCPOAuthService.

getMCPConfig(serverId)

Returns the connection config for starting an MCP session:

{
  command?: string;    // For STDIO transport
  args?: string[];
  url?: string;        // For HTTP/SSE transport
  transport: 'stdio' | 'sse' | 'http';
}

MCPConfig Schema

FieldTypeDescription
idstringPrimary key
orgIdstringOwning organisation
userIdstringOwning user (required)
serverIdstringProvider slug
instanceIdstringUnique instance ID
labelstringHuman-readable name
hostingCLOUD | LOCALWhere the MCP process runs
transportSTDIO | SSE | HTTPConnection transport
configJSON?Non-sensitive config
credentialsJSON?Encrypted OAuth tokens or API key
statusstringCONNECTED | DISCONNECTED | CONNECTING | ERROR
lastErrorstring?Last error message
lastUsedAtDateTime?Last usage

MCP OAuth Module

Server-mediated OAuth for integrations like Notion, Google Drive, and ClickUp. Source: packages/server/src/modules/mcp-oauth/.

OAuth Endpoints

MethodPathGuardsDescription
POST/mcp-oauth/startClerkAuthGuardStart OAuth flow → get authUrl
GET/mcp-oauth/callbackNoneOAuth provider callback
POST/mcp-oauth/refreshNoneRefresh an expired token
GET/mcp-oauth/pollNonePoll for credentials after callback

OAuth Flow

Client          Server           OAuth Provider
  │                │                  │
  ├─POST /start───▶│                  │
  │◀─── authUrl ───┤                  │
  │                │                  │
  │    (user opens browser)           │
  │                │◀─ GET /callback ─┤
  │                ├─ exchange code ──▶│
  │                │◀─── tokens ───────┤
  │                │ (store credentials)
  │                │                  │
  ├─GET /poll ────▶│                  │
  │◀─ credentials ─┤                  │
  1. POST /mcp-oauth/start: Client sends { serverId, instanceId, state }. Server looks up provider's authUrl + clientId + scopes, constructs the full URL, and returns it.
  2. Client opens URL in browser. User grants access. Provider redirects to GET /mcp-oauth/callback?code=...&state=....
  3. Server exchanges the code for tokens using the provider's tokenUrl.
  4. Credentials are stored temporarily (keyed by state) pending the client's poll.
  5. GET /mcp-oauth/poll?state=...: Client polls until { ready: true, credentials: {...} }.
  6. Client calls POST /connectors/:id/credentials to persist the credentials.

MCPOAuthService

MethodDescription
startFlow({ serverId, instanceId, state, orgId })Build and return auth URL
handleCallback(state, code)Exchange code, store credentials temporarily
refreshToken(serverId, refreshToken)Refresh using stored client secret
pollCredentials(state)Return { ready, credentials? }