fibric Docs fibric.io →
Reference preview
Reference preview

These pages describe Fibric’s target architecture. Public CLI, API, SDK, and sandbox access are not available. Examples do not establish deployed capabilities.

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

Tool handlers, event pollers, and probes receive a ConnectorCtx. The reference context supplies tenant-scoped services for the integration.

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.

The reference runtime provides a tenant-scoped HTTP client through ctx.http. It resolves the configured credentials and applies its rate and retry policy. Validate those services in the actual runtime before depending on them.

#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 },
  }),
});

The example uses capability names rather than vendor endpoints, marks side-effecting tools explicitly, and leaves credential resolution to the runtime. A connector must accurately declare whether a tool can change the target system.

#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

The reference executor owns policy, coordination, and duplicate-request checks. The handler implements the target-system request and the outcome-reporting contract. Review any additional downstream constraints separately.

#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