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.
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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Stable 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. |
version | string | yes | Semver string, for example "1.2.0". Published versions are immutable; a fix ships as a new version. |
category | ConnectorCategory | yes | One of the categories below. Drives marketplace placement and default policy templates. |
publisher | 'first-party' | 'partner' | 'private' | no | Who ships the connector. private connectors never appear in the marketplace and are visible only inside your workspace. Defaults to private until you publish. |
auth | AuthSchema | yes | How 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. |
tools | Record<string, ToolDef> | yes | The capabilities this connector exposes, keyed by tool name, for example "conversation.read". Each value is built with the tool() helper. |
events | Record<string, { kind: 'webhook' | 'poll'; topic?: string }> | no | The 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?: ... } | no | Health 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.
export type ConnectorCategory =
| 'crm'
| 'commerce'
| 'voice'
| 'shipping'
| 'comms'
| 'data'
| 'hardware'
| 'ai-operator';
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.
export interface ConnectorCtx {
tenant_id: string;
reseller_id: string | null;
config: Record<string, unknown>;
log: (msg: string, extra?: Record<string, unknown>) => void;
}
| Field | Type | Description |
|---|---|---|
tenant_id | string | The tenant this invocation belongs to. Stamped by the runtime; never supplied by the caller. |
reseller_id | string | null | The reseller above the tenant, or null for Fibric-direct tenants. Present on every envelope and every row for the same reason. |
config | Record<string, unknown> | Non-secret, per-connection configuration: a subdomain, a region, a default queue. Secrets never travel here. |
log | (msg, extra?) => void | Structured 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.
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.
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
- Tools & auth: the
tool()helper and auth declarations in depth, including validation and secret handling. - Testing connectors: replay recorded envelopes and assert on proposed plans without executing anything.
- Publishing to the marketplace: listing metadata, review, and the status lifecycle.
- Governance & trust: what the executor does with a side-effecting tool call.