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

# Build a plugin

> Scaffold, author, validate, and smoke-test an Aperium integration plugin.

A **plugin** is how Aperium learns to talk to a system. It's a Python package that exports
one factory function returning a `PluginManifest` — a declaration of the plugin's identity,
how it authenticates, which capabilities and tools it exposes, and how the host should
surface it. The runtime discovers plugins through the `aperium.plugins` entry-point group;
transports (MCP, HTTP, in-process) attach as adapters on top of a manifest — they are never
the plugin itself.

<Note>
  This page is for building a first-party plugin in the codebase. If you just want to wire
  Aperium to an external system that already speaks MCP, an admin can register it through
  the UI with no code — see [Custom integrations](/admins/integrations/custom).
</Note>

The [`aperium-plugin-example`](#the-worked-example) plugin is the canonical copy-me
template and the worked example throughout this page.

## 1. Scaffold

```bash theme={null}
uv run dev plugins new my_integration
```

The slug matches `^[a-z][a-z0-9_-]*$` and must equal the entry-point name. The scaffolder
lays out the package under the `aperium.plugins.<slug>` namespace and registers the entry
point in the new package's `pyproject.toml`:

```toml theme={null}
[project.entry-points."aperium.plugins"]
my_integration = "aperium.plugins.my_integration:build_manifest"
```

Sync the workspace so the new package is importable:

```bash theme={null}
uv run dev install
```

## 2. Author the manifest

Every plugin exports a `build_manifest(ctx) -> PluginManifest` factory at its package top
level. It is a **pure factory**: resolve secrets and wire dependencies, but do no I/O at
factory-call time — I/O belongs in `lifecycle` hooks or tool handlers.

```python theme={null}
def build_manifest(ctx: PluginContext) -> PluginManifest:
    return PluginManifest(
        identity=PluginIdentity(
            name="my_integration",
            version="0.0.1",
            display_name="My Integration",
            description="One-line blurb that surfaces in routing and docs.",
            owner="my-team",
        ),
        auth=PluginAuth(type=AuthType.NONE),
        capabilities=[...],
        tools=[...],
    )
```

The manifest is where the real work is. The essentials:

<Steps>
  <Step title="Identity and auth">
    Declare who the plugin is (`PluginIdentity`) and how it authenticates. Use `auth_shape`
    and `connection_shapes` for new plugins so onboarding and health sweepers can project
    the setup card. Credentials are resolved at runtime via `ctx.secrets` — the manifest
    only *declares* what's needed, never the values.
  </Step>

  <Step title="Capabilities">
    `CapabilityMetadata` entries are routing metadata: `domains`, `entities`, `operations`,
    `services`, `routing_hints`, and 2-3 `routing_examples` per user-facing capability.
    Tools reference a capability by name. Write-class tools must live under a capability
    with `approval_required=True`.
  </Step>

  <Step title="Tools">
    Each `ToolSpec` binds an input model, an output model, a handler, a `RiskClass`, and a
    capability. Handler signatures are validated strictly against the schemas at
    construction — a drift fails the manifest build.
  </Step>

  <Step title="A query profile">
    Every read-capable plugin must declare a query profile (enforced by the
    `check_query_profile` gate). The simplest path: expose one READ tool that returns a
    single list of flat, scalar-field rows — the SDK auto-adapts it into a queryable
    dataset. Providers with a native query language declare a `DatasetPushdown` instead.
  </Step>
</Steps>

See the [Manifest reference](/develop/manifest-reference) for every field and its rules.

## 3. Validate

```bash theme={null}
uv run dev plugins validate my_integration
```

Validation builds the manifest through real entry-point discovery and runs the manifest
invariants (unique tool/capability names, resolvable capability references, handler-schema
agreement, write-class approval rules, query-profile coverage) plus import-linter's
plugin-isolation contract.

## 4. Test

Plugin tests live under your package's `tests/` directory. The SDK ships a public test
harness (`aperium.plugin_sdk.testing`) with in-memory fakes so real handlers run unchanged:

```python theme={null}
signed = FakeSignedClient(responses=[FakeResponse(payload={"id": "1"})])
ctx = FakeExecutionContext(
    signed_client=signed,
    connection_metadata={"base_url": "https://api.example.com"},
)
out = await my_tool(MyInput(...), ctx)
assert signed.calls[0]["method"] == "GET"
```

Run them through the workspace test suite:

```bash theme={null}
uv run dev test fast
```

For plugins that replay recorded provider traffic, record cassettes with
`uv run dev plugins record my_integration`.

## 5. Smoke

```bash theme={null}
uv run dev plugins smoke
```

`smoke` loads every shipped plugin manifest the way the host does at startup — the final
check that your plugin registers cleanly alongside the rest of the fleet.

## The worked example

The `plugins/aperium-plugin-example` package is the smallest viable, copyable plugin. It's
a fixture-only reference (never auto-enabled on a tenant) that exercises the
full manifest surface so you can read one working plugin end to end:

* A `build_manifest(ctx)` factory with a complete `PluginIdentity`, `auth_shape`,
  `connection_shapes`, `ui` metadata, and a setup guide.
* An `echo` READ tool and a `list_echoes` tool whose flat-row output auto-adapts into the
  plugin's query profile.
* One capability with routing hints, one background `JobSpec`, and a verified webhook spec.

Copy it, rename the slug and namespace, then replace the reference tools with your own.

<Tip>
  Keep tool output models flat where you can — a single list of scalar-field rows is what
  lets the SDK auto-adapt a READ tool into a query dataset with no extra declaration.
</Tip>

## Related

<CardGroup cols={2}>
  <Card title="Manifest reference" icon="file-code" href="/develop/manifest-reference">
    Every model and field on the PluginManifest surface.
  </Card>

  <Card title="Custom integrations" icon="code" href="/admins/integrations/custom">
    Register an external MCP server through the Admin Console — no code.
  </Card>
</CardGroup>
