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

Connector manifest

A connector has no separate manifest file: the ConnectorDef you pass to defineConnector() is the manifest. The platform reads it to register capabilities, publish the marketplace listing, wire event streams, and decide which tools route through the deterministic executor. This page documents every field of that schema, the capability declarations it implies, delivery over MCP, and the versioning rules the registry enforces.

The manifest is the definition

The whole schema, exactly as exported from @fibric/connector-sdk:

@fibric/connector-sdk — 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 } };
}

export function defineConnector(def: ConnectorDef): ConnectorDef;

defineConnector() returns the def unchanged. There is no hidden registration side effect and no build step that transforms it; what you export is what the platform reads. That flatness is deliberate: the def can be reviewed in a pull request, diffed between versions, and validated by fibric connectors test without executing a handler.

Field reference

FieldTypeRequiredDescription
idstringyesStable identifier, unique across the registry. Convention: cn- prefix for connectors, op- for operator packs.
versionstringyesSemver. Published versions are immutable; see versioning.
categoryConnectorCategoryyesOne of eight values; drives marketplace placement and capability namespacing.
publisher'first-party' | 'partner' | 'private'noWho stands behind the listing. Omitted means private: visible only inside your tenant.
authAuthSchemayesHow credentials are obtained, as a declaration of kind. Never carries material. See auth patterns.
toolsRecord<string, ToolDef>yesThe capabilities this connector provides, keyed by capability name. See exposing actions.
eventsRecord<string, {...}>noThe event streams this connector emits into the envelope bus. See emitting events.
probe(ctx) => {...}noHealth check the platform calls to render connection status.

id

The id is permanent. It appears in receipts (action.connector), in capability bindings, and in marketplace URLs; changing it is publishing a different connector. Use a short, lowercase, hyphenated name with the type prefix: cn-kustomer, cn-magento, cn-amazon-connect are the live first-party examples. The registry rejects an id already claimed by another publisher.

version

Strict semver, no prerelease tags in published listings. The version in the def is the version the registry records at fibric publish time; there is no separate package version to drift from it.

category

ConnectorCategory
export type ConnectorCategory =
  | 'crm'
  | 'commerce'
  | 'voice'
  | 'shipping'
  | 'comms'
  | 'data'
  | 'hardware'
  | 'ai-operator';
CategoryCoversLive example
crmHelpdesks, CRM, support platformscn-kustomer
commerceOrder and catalog systemscn-magento
voiceTelephony and contact-center platformscn-amazon-connect
shippingCarriers and fulfillment
commsMessaging, email, notifications
dataWarehouses, feeds, generic data sources
hardwareBuilding systems, sensors, industrial gateways
ai-operatorOperator packs; operators are connectors tooop-order-sentinel, op-radar-analyst

The category is coarse on purpose. Fine-grained matching happens through capability names, not categories; the category is for humans browsing the marketplace and for review routing.

publisher

first-party is reserved for Fibric-maintained connectors. partner listings carry the publisher's name and go through marketplace review. private connectors skip review entirely and are installable only within the publishing tenant, which is the right setting for internal systems that will never be listed.

auth

An AuthSchema is a kind plus optional OAuth scopes; it never carries credential material, and no field of the def accepts any. The helpers oauth2(), apiKey(), and none() cover the common kinds; basic, aws_iam, and mtls are declared as literals. The full treatment, including rotation and secret storage, is on connector auth patterns.

tools

The tools record is the connector's capability surface. Keys are the capability names operators will request ('conversation.read', 'order.hold'); values are ToolDefs built with tool(). The one field the registry cares deeply about is sideEffecting: it decides, at registration time and immutably per version, whether a tool runs inline as a read or is reachable only through a validated ExecutionPlan. The handler contract, input validation, and idempotency rules are on exposing actions.

events

Each entry declares one stream the connector turns into EventEnvelopes, keyed by the event_type it will emit. kind: 'webhook' means the source pushes and the platform provisions an ingest URL per connection; kind: 'poll' means the platform schedules pulls, which is the shape for hardware and for APIs without webhooks. The optional topic names the upstream subscription (a webhook topic, an MQTT topic, a queue). Field-level rules for the envelopes you emit are on emitting events.

probe

The probe is a cheap, read-only health check the platform calls periodically and on the connection settings screen. Return status: 'ok' when a lightweight authenticated call succeeds, anything else to surface degradation, and optionally one metric worth showing next to the status light.

probe
probe: (ctx) => ({
  status: 'ok',
  metric: { label: 'open conversations', value: 214 },
}),
!
Probes must not write

The probe runs outside the executor, so it must be strictly read-only. A probe that mutates the target system is a governance bypass and fails marketplace review.

Capability declarations

Tool keys are not private names; they are the public vocabulary of the capability layer. When your def declares 'order.hold', the registry records that this connector provides the order.hold capability, and any operator requiring it can be bound to your connector without either side naming the other. Two rules follow:

what the registry derives from a def
// from this def...
tools: {
  'conversation.read': tool({ handler: readConversation }),
  'note.write':        tool({ sideEffecting: true, input: NoteArgs, handler: writeNote }),
},
events: {
  'conversation.created': { kind: 'webhook', topic: 'conversations' },
},

// ...the registry records:
//   provides capability  conversation.read   (read)
//   provides capability  note.write          (governed write)
//   emits    event_type  conversation.created

Delivery over MCP

A registered connector is served as an MCP server. Each entry in tools becomes an MCP tool with the same name; the input validator's shape is surfaced as the tool's input schema; events feed the envelope bus rather than the MCP session. You do not write any MCP plumbing, and you cannot opt out of it: the protocol surface is generated from the def, which is what keeps a helpdesk, a BACnet gateway, and an operator pack speaking one protocol.

Two consequences worth knowing. First, tool names must be valid MCP tool identifiers, which the noun.verb convention already satisfies. Second, the governance boundary is preserved across the protocol: a side-effecting MCP tool call from any client still lands in the executor and is disposed under the tenant's trust policy. MCP is a transport, not a back door.

Versioning

The registry treats the def as an interface and holds versions to semver discipline:

ChangeBumpNotes
Bug fix inside a handler; probe changes; log changespatchNo visible contract change. Auto-applied within the installed minor line.
New tool or event; new optional input field; widened enumminorAdditive. Existing bindings are unaffected; new capabilities become bindable.
Removed or renamed tool or event; narrowed input; changed sideEffecting; changed auth.kindmajorBreaking. Installed connections stay on their version until an operator of the tenant re-binds explicitly.

Published versions are immutable, and receipts record the connector version that executed every action, so an audit can always answer which contract a side effect ran under. Flipping sideEffecting in either direction is always major: it moves a tool across the governance boundary, which is the most consequential change a def can make.

A complete manifest

Every field in use, in the shape review expects:

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

const NoteArgs = z.object({
  conversation_id: z.string().min(1),
  body: z.string().min(1).max(4000),
});

export default defineConnector({
  id: 'cn-brightdesk',
  version: '1.2.0',
  category: 'comms',
  publisher: 'partner',
  auth: oauth2({ scopes: ['conversations.read', 'notes.write'] }),

  tools: {
    'conversation.read': tool({
      input: (a) => ({ conversation_id: z.string().parse((a as any).conversation_id) }),
      handler: async (ctx, args) => {
        ctx.log('read', { id: args.conversation_id });
        // return await ctx.http.get(`/v1/conversations/${args.conversation_id}`);
        return { id: args.conversation_id, status: 'open' };
      },
    }),
    'note.write': tool({
      sideEffecting: true,
      input: (a) => NoteArgs.parse(a),
      handler: async (ctx, args) => {
        // return await ctx.http.post(`/v1/conversations/${args.conversation_id}/notes`,
        //   { body: args.body });
        return { ok: true };
      },
    }),
  },

  events: {
    'conversation.created': { kind: 'webhook', topic: 'conversations' },
    'conversation.updated': { kind: 'webhook', topic: 'conversations' },
  },

  probe: (ctx) => ({ status: 'ok', metric: { label: 'open conversations', value: 214 } }),
});

Keep going