Skip to content

Plugin reference

This is the detailed contract for extending Precursor. For a friendly overview, see the Plugins feature guide.

Work in progress — not yet tested

The plugin system is experimental and unverified. The entry-point loader and frontend extension registry exist in the codebase, but the flow has not been validated end-to-end with a real plugin, and not every slot or dynamic renderer mount is wired in core yet. The contract may change without notice before 1.0 — don't rely on it for production plugins yet.

Design principle

Precursor is intentionally small. Anything that is not part of "topics, chat, GitHub" should live in a plugin rather than growing core.

Backend contract

A plugin is a Python package that exposes a register(registry) callable and declares it as an entry point in the precursor.plugins group:

toml
# pyproject.toml of the plugin package
[project.entry-points."precursor.plugins"]
my_plugin = "my_pkg.precursor_plugin:register"

The register function receives a PluginRegistry and can:

  • mount routersregistry.add_router(router) adds a FastAPI APIRouter (namespace it under /api/<your-plugin> to avoid collisions), and
  • contribute frontend extensionsregistry.add_frontend_extension(...) registers a descriptor served from /api/plugins.
python
# my_pkg/precursor_plugin.py
from fastapi import APIRouter
from precursor.backend.plugins import FrontendExtension, PluginRegistry

router = APIRouter(prefix="/api/my-plugin", tags=["my-plugin"])


@router.get("/ping")
async def ping():
    return {"ok": True}


def register(registry: PluginRegistry) -> None:
    registry.add_router(router)
    registry.add_frontend_extension(
        FrontendExtension(
            id="my-plugin.panel",
            kind="panel",
            slot="topic.sidebar.bottom",
            title="My panel",
            config={"endpoint": "/api/my-plugin/ping"},
        )
    )

Plugins are discovered once on startup by discover() (called from the FastAPI lifespan). A failing plugin is logged and never crashes the host.

Frontend contract

The SPA fetches /api/plugins on boot and stores the descriptors. To render a contributed extension, register a renderer for its kind:

ts
// frontend/src/main.tsx (in your fork or a separate bundle)
import { registerRenderer } from "./lib/plugins";
import { MyPanel } from "./my-plugin/MyPanel";

registerRenderer("panel", MyPanel);

MyPanel receives the descriptor (id, slot, title, config) and is mounted wherever the SPA renders that slot.

FrontendExtension descriptor

FieldTypePurpose
idstringStable unique id (e.g. my-plugin.panel).
kindstringWhich renderer handles it (see below).
slotstringWhere in the SPA it mounts.
titlestringHuman-readable label.
configobjectFree-form config passed to the renderer (e.g. an endpoint).

Designed plugin kinds

KindSlot examplesUse case
paneltopic.sidebar.bottomSide-by-side context (e.g. a PR diff).
message-rendererchat.message.bodyMermaid / drawio / chart blocks.
settings-tabsettings.tabsPer-plugin configuration UI.
topic-actiontopic.header.actionsButtons that operate on the topic.

MCP tools from a plugin

Because Precursor is an MCP client, a plugin can also register external MCP tool servers, so a plugin can contribute new tools the assistant can call without touching core.

Released under the MIT License.