Fibric. Docs fibric.io →
v1.0.0 ยท stable
Build

defineConnector()

defineConnector() is the single entry point of the Connector SDK. You hand it one declarative object, a ConnectorDef, and that object is the entire contract: the tools the connector exposes, the events it emits, how it authenticates, and how the platform checks its health. The runtime supplies everything else: per-tenant credentials, rate-limited HTTP, idempotency, and tracing. This page documents every field of the def, with types taken directly from @fibric/connector-sdk.

Signature

defineConnector() takes a ConnectorDef and returns it unchanged. It exists so the compiler checks your def against the contract and so the CLI, the registry, and the marketplace can all read the same shape. There is no hidden registration step and no runtime magic. The def is data.

@fibric/connector-sdk
export function defineConnector(def: ConnectorDef): ConnectorDef;

export interface ConnectorDef {
  id: string;
  version: string;
  category: ConnectorCategory;
  publisher?: 'first-party' | 'partner' | 'private';
  auth: AuthSchema;
  tools: Record<string, ToolDef>;
  events?: Record<string, { kind: 'webhook' | 'poll'; topic?: string }>;
  probe?: (ctx: ConnectorCtx) => { status: string; metric?: { label: string; value: unknown } };
}

ConnectorDef fields

FieldTypeRequiredDescription
idstringyesStable identifier for the connector, for example cn-kustomer. It names the connector on every PlannedAction, every receipt, and every trust policy rule, so choose it once and do not change it.
versionstringyesSemver string, for example "1.2.0". Published versions are immutable; a fix ships as a new version.
categoryConnectorCategoryyesOne of the categories below. Drives marketplace placement and default policy templates.
publisher'first-party' | 'partner' | 'private'noWho ships the connector. private connectors never appear in the marketplace and are visible only inside your workspace. Defaults to private until you publish.
authAuthSchemayesHow credentials are obtained, declared with the auth helpers. The def declares the shape; the runtime resolves the actual secret per tenant at call time. See Tools & auth.
toolsRecord<string, ToolDef>yesThe capabilities this connector exposes, keyed by tool name, for example "conversation.read". Each value is built with the tool() helper.
eventsRecord<string, { kind: 'webhook' | 'poll'; topic?: string }>noThe sense side: named event streams the connector emits as event envelopes. Each entry declares how the stream is fed: webhook for push, poll for pull.
probe(ctx: ConnectorCtx) => { status: string; metric?: ... }noHealth check the platform calls on a schedule. Return a short status and, optionally, one labelled metric shown on the connector's status card.

ConnectorCategory

The category is a closed union. It tells the marketplace where the connector lives and tells policy tooling what kind of blast radius its side effects have. A hardware connector gets a more conservative default policy template than a data connector.

ConnectorCategory
export type ConnectorCategory =
  | 'crm'
  | 'commerce'
  | 'voice'
  | 'shipping'
  | 'comms'
  | 'data'
  | 'hardware'
  | 'ai-operator';
i
Operators are connectors too

The category ai-operator is not an afterthought. An operator ships through the same defineConnector() shape as a SaaS integration or a BACnet gateway: same auth declaration, same tools, same events. That is what "everything is MCP" means in practice. See Operator packs for how operators are packaged on top of this.

ConnectorCtx

Every tool handler, event poller, and probe receives a ConnectorCtx. It is the connector's whole view of the world, and it is tenant-scoped by construction: there is no API on the context that can reach another tenant's data or credentials.

ConnectorCtx
export interface ConnectorCtx {
  tenant_id: string;
  reseller_id: string | null;
  config: Record<string, unknown>;
  log: (msg: string, extra?: Record<string, unknown>) => void;
}
FieldTypeDescription
tenant_idstringThe tenant this invocation belongs to. Stamped by the runtime; never supplied by the caller.
reseller_idstring | nullThe reseller above the tenant, or null for Fibric-direct tenants. Present on every envelope and every row for the same reason.
configRecord<string, unknown>Non-secret, per-connection configuration: a subdomain, a region, a default queue. Secrets never travel here.
log(msg, extra?) => voidStructured logging, correlated to the run and the tenant. Prefer it over console.log; it lands in traces the receipt can point at.

In production the runtime also mounts ctx.http: a per-tenant HTTP client that is rate-limited, retrying, and pre-authenticated with credentials resolved from the tenant's secret store. Your handler never sees the raw API key. The client injects it. That is the load-bearing rule of secret handling: the def declares the auth shape, the runtime holds the material.

A complete worked example

The connector below integrates a helpdesk in the shape of the live Kustomer connector: conversations flow in as events, reads run inline, and the one write, adding a note to a conversation, is side-effecting and therefore routes through the governed executor. This is a full, publishable def, not a fragment.

connectors/brightdesk/index.ts
import { defineConnector, tool, apiKey } from '@fibric/connector-sdk';

export default defineConnector({
  id: 'cn-brightdesk',
  version: '1.0.0',
  category: 'comms',
  publisher: 'partner',

  // The def declares the auth SHAPE. The runtime resolves the actual key
  // per tenant from the secret store at call time.
  auth: apiKey(),

  tools: {
    // Read: runs inline, no policy check, no idempotency needed.
    'conversation.read': tool({
      input: (args) => {
        const a = args as { conversation_id?: unknown };
        if (typeof a.conversation_id !== 'string') {
          throw new Error('conversation_id: string required');
        }
        return { conversation_id: a.conversation_id };
      },
      handler: async (ctx, args) => {
        ctx.log('reading conversation', { id: args.conversation_id });
        // ctx.http is pre-authenticated and rate-limited by the runtime
        // return await ctx.http.get(`/v1/conversations/${args.conversation_id}`);
        return { id: args.conversation_id, status: 'open', messages: [] };
      },
    }),

    // Write: sideEffecting routes this through the deterministic
    // executor, behind the trust policy and idempotency dedup.
    'note.write': tool({
      sideEffecting: true,
      input: (args) => {
        const a = args as { conversation_id?: unknown; body?: unknown };
        if (typeof a.conversation_id !== 'string') {
          throw new Error('conversation_id: string required');
        }
        if (typeof a.body !== 'string' || a.body.length === 0) {
          throw new Error('body: non-empty string required');
        }
        return { conversation_id: a.conversation_id, body: a.body };
      },
      handler: async (ctx, args) => {
        ctx.log('writing note', { id: args.conversation_id });
        // return await ctx.http.post(`/v1/conversations/${args.conversation_id}/notes`,
        //   { body: args.body });
        return { ok: true };
      },
    }),
  },

  // The sense side: streams that become event envelopes.
  events: {
    'conversation.created': { kind: 'webhook', topic: 'conversations' },
    'conversation.updated': { kind: 'webhook', topic: 'conversations' },
    'sla.breached':         { kind: 'poll' },
  },

  // Health, shown on the connector's status card.
  probe: (ctx) => ({
    status: 'ok',
    metric: { label: 'open conversations', value: 42 },
  }),
});

Three decisions in this file carry all the weight. First, the tool names are capability verbs, conversation.read and note.write, not vendor endpoints; an operator that requires note.write works against any connector that provides it. Second, sideEffecting: true is the only line that separates a read from a write, and it changes everything about how the call is treated. Third, the handler holds no credentials and no retry logic, because both belong to the runtime.

Lifecycle

A connector moves through four phases from source file to serving traffic. Each phase reads only the def; there is no separate manifest to keep in sync.

1. Register

You add the connector to a workspace, from a local directory during development (fibric connectors add ./connectors/brightdesk) or from the marketplace by id. The platform reads the def, records the tool and event names, and indexes the capabilities so operators can bind against them. Nothing has authenticated yet and nothing can run.

2. Handshake

A connection is created for one account of the target system. The auth declaration drives what happens: apiKey() prompts for a key that goes straight into the tenant's secret store, oauth2() runs the authorization flow with the declared scopes. On success the platform calls probe once to confirm the credentials actually work before marking the connection healthy.

3. Sense stream

Declared events go live. Webhook streams get a per-connection endpoint; poll streams get a schedule. Every emission is normalized into an EventEnvelope, stamped with tenant_id, reseller_id, source, and event_type, and published to the bus, where the router matches it against operator triggers like conversation.*.

4. Act tools

Tools become invocable. Reads are called inline by operators sensing state. Side-effecting tools are reachable only through a validated ExecutionPlan: the deterministic executor checks the trust policy, takes the single-flight lock on the action's entity_key, dedupes on its idempotency_key, and only then invokes your handler. Every disposed action leaves a receipt.

!
Your handler is not the safety layer

Do not build policy checks, dedup, or locking into the handler. The executor already provides all three, and a handler that second-guesses them makes behavior harder to audit. The handler's job is one honest call to the target system, and an exception when that call fails.

Connectors are MCP servers

A Fibric connector is an MCP server. The tools record maps directly to MCP tool declarations, the events record to its notification streams, and the def's metadata to the server's self-description. This is why one SDK shape covers a helpdesk, a Modbus gateway, and an AI operator: the kernel never hardcodes an integration, it speaks MCP to all of them, and the governed executor sits in front of every side-effecting tool regardless of what is on the other end.

The practical consequence is symmetry. Anything that can present an MCP tool surface can be a connector, and every connector is automatically usable by anything that speaks MCP, subject to the same trust policy. The moat is not the protocol; it is the governance wrapped around it.

Keep going