> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add a Connector with the Connector SDK

> Implement a Connector, test it hermetically with MockConnector, and see exactly what registering it into the default server requires.

<Info>**\[AVAILABLE]**, every code block below was run in this session against `@parmana/connector-sdk`, commit `651497a`.</Info>

## 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](/concepts/the-gateway) and [Credential isolation](/concepts/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.

```typescript theme={null}
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:

```typescript theme={null}
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

<Warning>
  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:

  ```typescript theme={null}
  // 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.
</Warning>

## 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

<CardGroup cols={2}>
  <Card title="Issue and verify session credentials" icon="key" href="/guides/session-credentials">
    What your connector's `context.credential` actually is, and its lifecycle.
  </Card>

  <Card title="Credential isolation" icon="shield" href="/concepts/credential-isolation">
    The concept this guide's credential handling exercises.
  </Card>
</CardGroup>
