Skip to main content
The marketplace is the ecosystem’s plugin index. A running server browses it, installs a plugin from it in one command, and hears from it when a published version turns out to be bad. This page explains the model. To search it and install from it, start at Marketplace.

A metadata index

The registry stores metadata, never packages. A listing points at the plugin’s real distribution channel — usually a pip package and a source repository, or, for a descriptor-only plugin, the tai-plugin.yml itself — and installing resolves that pointer and installs by source: a GitHub-sourced release is downloaded and its SHA-256 verified against the digest the registry recorded at ingest, while a PyPI-sourced release pins the exact version and relies on PyPI’s file immutability. A third source, spec, backs a descriptor-only listing that ships no package at all — its artifact is the tai-plugin.yml itself, pinned at its tag and digest. The registry’s catalog is curated: it lists the ecosystem’s first-party plugins, and every listing carries an official trust tier.

Listings and items

One installable plugin carries many capabilities: a single package can ship tools, extensions, and agents together. The registry indexes each contained capability as its own item row with a kind, categories, and tags — because an item is what you search for. You search at item level and install at plugin level: finding the generate_uuid tool leads you to install the listing that carries it. An item’s kind decides how it wires into a server, and it also decides whether the item is code or data. Most kinds are code: the item names a Python module the server imports. Two kinds are data — mcp-server and connector — where the item carries a declarative block (an MCP transport entry, or a connector’s provider descriptor) instead of a module, so nothing has to be imported to register it. That data-versus-code split is independent of how the plugin is delivered, below.

Versions

Each listing publishes versions. A version records the exact release, its artifact, and the tai42-contract compatibility range it declares, so a server can tell whether a release fits its contract before installing. The registry validates every release’s artifact against its listing before publishing it — a release that fails validation is never served.

Advisories and the kill switch

An advisory flags affected versions of a listing with a severity and a summary. Separately, a version can be killed — a registry-side kill switch that refuses to resolve that version at all, so it can never be installed. The two are distinct: an advisory is a published warning that can later be withdrawn, while a killed version is pinned out of resolution. See Advisories for how a server checks.

The plugin descriptor: tai-plugin.yml

Every plugin describes itself with a tai-plugin.yml — the plugin descriptor — at its repository root and, when it ships one, inside its built wheel. It names the listing, the version, the contract range, the provides list of every item the plugin carries, and — for a plugin that owns database tables — the migrations folder that holds its migration chain. It also names the package, the pip distribution that backs the listing — but package is optional: a plugin whose every item is data can omit it and ship no code at all (see Delivery). The registry indexes a plugin from this file and validates the artifact against it; the installer uses the provides list to patch the server manifest per provided item. It is a different file from the server manifest: tai-plugin.yml describes what a plugin offers, the server manifest declares what a server loads.

Delivery: package or descriptor

A listing carries two independent axes. Payload is what each item is — code or data, decided by its kind (above). Delivery is how the plugin arrives, and it is set by one field: package.
  • Package delivery (package present) — the listing points at a pip distribution. Installing pip-installs it into the server’s environment, exactly as every code plugin does. A data plugin may still ship a package: an mcp-server whose command is a console script the wheel installs, for example, is data-payload and package-delivered. Delivery is independent of payload.
  • Descriptor delivery (package absent) — a descriptor-only plugin. Every item is data, so there is nothing to import and nothing to pip-install. The registry lists it with source: spec: the artifact is the tai-plugin.yml itself, pinned at its tag and digest, with no wheel and no PyPI lookup. Today a connector ships this way — a whole OAuth integration as one descriptor, no code.
delivery is derived, never declared: it reads descriptor when package is absent and package when it is present, and every surface — the CLI, the Studio, and the marketplace detail — shows that one word so you always know whether an install will touch the environment.

Declared HTTP routes

A plugin that mounts HTTP routes — a router item, or a channel item with an inbound webhook — declares them in its descriptor rather than wiring absolute paths in code. A router item must carry a routes block; a channel item may; no other kind does. The block is two parts:
  • base — the item’s default mount prefix, relative and template-free (one or more [a-z0-9-] segments joined by /). The resolved absolute path of every route is /api/ + base + the route’s own path. The /api/ root is fixed platform-wide; only base moves, and the operator can remap it at install — always, and as the collision remedy.
  • paths — the route rows. Each row is a path relative to base, a non-empty methods list, and a required public flag. A path is /-prefixed with at least one segment; a segment is a literal or a {name} template. public has no default — the declaration always states whether the resolved route answers unauthenticated.
tai-plugin.yml
Here the webhook resolves to /api/alerts/relay/inbound (public, so it answers without a credential) and the status read to /api/alerts/relay/status/{event_id} (authed). The item’s route module registers each handler against the relative path; the declaration decides the absolute mount and the public-ness, so the code never hardcodes either. Each declared route has one owner. Two routes collide when their resolved path shapes overlap — same segment count, and at every position both are the same literal or one is a template — and they share a method (GET /api/x/{id} overlaps GET /api/x/foo; a different method or a different segment count never collides). The server refuses to register a route that collides with one a different plugin or the core already owns, and install surfaces the clash up front with remap as the remedy. How a declared public route is served without a credential is the declared-public tier.

The migrations contract

A plugin that owns tables ships its schema as a migration chain, not a one-shot DDL apply. The framework runs the chain: tai db migrate discovers every component — the core plus each installed plugin that declares one — and applies its pending files in order. A plugin author owns the files; the runner owns the mechanism. Declare the folder. Point the descriptor’s migrations field at a directory packaged inside the wheel:
tai-plugin.yml
Number the files. Each file is NNNN_short_name.sql — one or more leading digits, an underscore, a name, the .sql suffix — applied in ascending numeric order. A .sql file that does not match the pattern, or two files claiming the same number, is a loud discovery error, never a silent skip.
The chain’s component identity. By default the chain is recorded in the per-database tai_schema_history table under the plugin’s distribution name (package) — for the accounts plugin, tai42-accounts-postgres — and that default identity never changes across releases, so the runner always reads the exact rows earlier versions wrote. A plugin whose feature store is a separate database component rather than the distribution itself may instead declare an explicit migrations_component naming that component (declaring it requires migrations; a component with no chain migrates nothing). A component-scoped chain runs only while that component’s database binding is explicitly declared: with the binding set, the chain migrates that component’s store under the declared name; with it unset, tai db migrate visibly skips the chain — a distinct, logged skip naming which chain did not run — rather than migrating it into the default database. So the store stays off until an operator opts it in by declaring its binding. Checksum immutability — never edit a shipped file. The runner records a SHA-256 of every applied file. On the next migrate or boot gate check it re-verifies each recorded version against the file that produced it; a changed checksum reads as a rewritten chain and is a hard error, never a silent re-run. A shipped migration is immutable. A correction — even a typo — is a new numbered file appended to the chain, never an edit to the old one. Chains roll forward only; there are no down-migrations. Breaking changes: expand then contract. Because there is no down-migration and older replicas may still be running during a rollout, split a breaking change across releases rather than rewriting a column in place:
  1. Expand — add the new shape additively (a new nullable column, a new table) in one migration. Old and new code both run against it.
  2. Backfill and cut over — ship code that writes both shapes, or backfills the new one, until every replica is on the new release.
  3. Contract — in a later release, once nothing reads the old shape, add a migration that drops it.
Each step is its own numbered file, so the chain records the whole transition and a from-scratch replay reproduces it exactly.

Installing into a server

Install is a server-side operation: the server resolves the listing, installs the verified package into its own environment, patches its manifest per provided item, and reloads. The server records the marketplace ref and installed version locally, so installed, update checks, and advisories answer from local truth. The whole flow is one CLI command or one Studio click — see Install a plugin. Because the install writes into the server’s own environment and manifest, a self-hosted deployment keeps both on persistent storage — the plugin-prefix directory and the manifest file — so a container recreation does not lose the plugin. See the persistent mounts.

Installing a descriptor-only plugin

A descriptor-only listing installs with no pip run: the server resolves the frozen tai-plugin.yml, patches the manifest — a connector adds one entry to the manifest connectors list — and reloads. Nothing lands in the environment except the config it needs. Because there is no package to carry secrets, a descriptor states the environment it needs and the server refuses to write it half-configured. The install preview computes the required_env for the listing — each variable as {name, secret} — and the missing_env still absent from the server. The Studio install dialog reads those and prompts for the missing values, seeding the secret toggles from required_env; the CLI takes them as flags:
--env KEY=VALUE supplies every value; --secret KEY is a bare mark that keeps a value masked and out of logs (a connector’s client secret is auto-marked too, and --secret stays available for any key you want hidden). An install that would leave a required variable unresolved is refused up front, naming the variables, before anything is written. Installing a descriptor mutates the running server the same as any install, so it sits behind the same admin-only trust boundary: scope the /api/marketplace/* routes with access control, and treat a descriptor’s official tier the way you treat a code plugin’s — a first-party connector is a curated, admin-installed integration, not an open door.

See also