Skip to main content
[AVAILABLE], every code block below was run in this session against @parmana/connector-sdk, commit 651497a.

Goal

Implement a Connector, verify it hermetically without a real target system, and understand precisely what’s required to reach the running default server, which today is a bootstrap code change, not configuration.

Prerequisites

  • Read The gateway and Credential isolation first, this guide assumes you know where a Connector sits in the pipeline and that it never resolves its own credential.

Steps

1. Implement the Connector interface

A Connector validates a request, executes using an already-resolved credential, and returns a deterministic response. It never evaluates policy, authorizes execution, or resolves a credential itself, credential resolution happens exclusively inside the gateway boundary, before a Connector is ever called.
import {
  connectorCapabilities,
  type Connector,
  type ConnectorExecutionContext,
  type ConnectorRequest,
  type ConnectorResponse,
} from "@parmana/connector-sdk";

class ExampleConnector implements Connector {
  readonly connectorId = "example";
  readonly capabilities = connectorCapabilities(["crm:read"]);

  async execute(request: ConnectorRequest, context: ConnectorExecutionContext): Promise<ConnectorResponse> {
    if (!this.capabilities.includes(request.capability)) {
      throw new Error(`does not declare capability "${request.capability}"`);
    }
    // context.credential is an opaque CredentialHandle, read it, never log it.
    return { success: true, metadata: { recordId: "example-1" } };
  }
}
ConnectorCapability is a namespaced verb (crm:read, payments:refund, http:post), validated eagerly by connectorCapabilities(), a malformed one throws at construction, not at execution time. By convention ExecutableContent.action is the capability string, note ConnectorRequest carries both a capability field and a separate action field, they’re expected to match.

2. Test it hermetically with MockConnector

MockConnector scripts a response without a real target system:
import { MockConnector, connectorCapabilities } from "@parmana/connector-sdk";

const mock = new MockConnector({
  connectorId: "example",
  capabilities: connectorCapabilities(["crm:read"]),
  script: { respond: () => ({ success: true, metadata: { recordId: "example-1" } }) },
});

const result = await mock.execute(
  { capability: "crm:read", businessTransactionId: "t1", action: "crm:read", target: "crm://record/1", parameters: {} },
  { credential: undefined as never },
);
Real output, this session:
mock result: { success: true, metadata: { recordId: 'example-1' } }
mock invocations: 1
mock.invocations records every ConnectorRequest the connector received, so a test can assert exactly what was executed. To test failure handling, use script: { failWith: new Error("upstream unavailable") } instead of respond.

Verify

Both calls above returned { success: true, metadata: { recordId: "example-1" } } and mock.invocations.length === 1, confirmed in this session. Version and health checks fail closed before your connector is ever invoked: SdkConnectorExecutor rejects execution if a configured expectedVersion doesn’t match the connector’s own metadata.version, or if metadata.health.status === "unavailable".

What reaching the default server actually requires

This is the part that’s easy to assume is configuration and isn’t. The default server registers exactly one connector, vendor-payment (packages/api/src/bootstrap/createVendorPaymentConnector.ts), following this real, running pattern:
// packages/api/src/bootstrap/createConnectorRegistry.ts (abridged, real source)
const registry = new ConnectorSdkRegistry();
registry.register({
  connector: createVendorPaymentConnector(),
  metadata: { connectorId: "vendor-payment", displayName: "Vendor Payment", version: { major: 1, minor: 0, patch: 0 }, health: { status: "healthy", checkedAt: new Date().toISOString() } },
  connectorIdentity: { connectorId: "vendor-payment", publicIdentity: "spiffe://parmana/connectors/vendor-payment", authenticationMetadata: {} },
  credentialProvider: createCredentialProvider(),
  policy: new CapabilityConnectorPolicy(new DefaultConnectorPolicy(authenticator, sessions)),
  gatewayAuthentication,
  crypto,
  audit,
});
Adding a second connector to the running server means editing three files: createConnectorRoute.ts (add a case mapping an action to your connector’s ID), createConnectorRegistry.ts (register it, following the shape above), and createConnectorAuthenticator.ts (trust its identity). There is no dynamic registration path, no environment variable that adds a connector, this is a code change today.

Troubleshoot

  • does not declare capability "..." thrown by MockConnector or your own connector. request.capability didn’t match anything in connectorCapabilities([...]). Check for a typo, capability strings are exact, case-sensitive matches.
  • SdkConnectorExecutor rejects with a version mismatch. An expectedVersion was configured at registration and your connector’s metadata.version doesn’t match, this guards a rolling deployment that swapped connector builds underneath a pinned expectation.
  • Registered your connector but POST /execute still says “No connector registered for action.” Registration in a ConnectorSdkRegistry you constructed yourself doesn’t reach the running default server, that server builds its own registry in createConnectorRegistry.ts, see the Warning above.

Next

Issue and verify session credentials

What your connector’s context.credential actually is, and its lifecycle.

Credential isolation

The concept this guide’s credential handling exercises.