This page covers the server manifest. A plugin’s own descriptor,
tai-plugin.yml, is a different file.Where it comes from
A server loads its manifest from the file pointed at by--manifest-path (its
env default is TAI_MANIFEST_PATH), or from its config directory. An empty
manifest ({}) boots a bare MCP server with no tools; every section is optional.
What it holds
Most entries are Python import paths. Importing a module runs its@tai42_app.*
registration decorators, which is how a tool, agent, or provider becomes live.
tools— each entry imports amoduleand selects tool names withinclude/exclude. A separateextensionsmap attaches an extension branch to a selected tool.agents— each entry imports a module whose@tai42_app.agents.agentdecorator registers an agent and generates its run tool.mcp— external MCP servers to mount, one transport per entry (url,uds, orcommand).- Startup module hooks — single-valued slots and module lists that wire
plugins in:
storage_module,backend_module,monitoring_module,extensions_modules,lifecycle_modules,routers_modules,middlewares_modules,webhook_verifier_modules, andchannel_modules.routers_modulesnames only extra HTTP routers mounted on top of the default set;default_routersselects that set (below). studio_plugins— names the installed Python packages that ship a Studio plugin (a browser-side UI extension), not import paths. Each named package carries its ownstudio-manifest.jsondeclaring what it contributes to the Studio shell — tool panels, pages, settings tabs, and sidebarnav_entries. That plugin manifest is a separate file from this server manifest; see Author a Studio plugin.api_tools— curates which management operations project as MCP tools (below). It never selects your own tools; it governs only the operation-projected surface.
A minimal manifest
The hello-world manifest names one local tool module and nothing else:module is an import path, so myapp must be importable when the server starts.
include limits which tool names from the module are exposed; omit it to expose
all of them.
Selection versus attachment
Within atools entry, include / exclude are selection only — they decide
which tool names go live. The extensions map is attachment — it branches a
selected tool into a new variant ({toolname: [ext]} produces toolname_ext).
Attachment never selects: mapping an extension onto a tool that is not in the
selected set raises loudly rather than silently doing nothing.
The api_tools block
The management operations — reload a tool, replace
the manifest, re-probe a failed MCP server — are always available as HTTP routes
and CLI commands. api_tools decides which of them also project as MCP tools:
enabled— withfalse, no operation projects as a tool; the surface is empty.expose_destructive— gates the destructive-flagged operations (a tool reload, a manifest replace). Withfalsethey stay off the MCP surface even though their routes remain.include/exclude— name operations to force on or suppress. A name appearing in both lists is a loud validation error, and anincludenaming an operation that is not registered fails boot rather than silently doing nothing.extensions— attaches extension combos to a projected operation, exactly as atoolsentry’sextensionsmap does.
run_tool, which runs any
tool by name — is never projectable: it is hardcode-blocked from the MCP surface
even when named in include. An authority-changing operation — one that mints
keys, edits policy, replaces the manifest (update_manifest), or restores
unshipped state (a backup import) — is off the default surface and projects only
when include names it explicitly.
HTTP routers
Out of the box the server mounts a default core-router set — every built-in API router, plus the Studio SPA catch-all served last — so a bare manifest boots the full Studio surface. You do not list the core routers.default_routers
selects the set:
all(the default) — mount the core API routers and serve the Studio SPA, whose catch-all route is added last. The normal full-Studio deployment.api— mount the core API routers but do not serve the SPA: a headless JSON HTTP API (/api/*) with no browser UI.none— mount no defaults;routers_modulesmust then list every router the server loads. For a fully manual or MCP-only surface.
routers_modules is always additive: it names extras — your own routers, a
plugin’s router — mounted on top of the selected default set. Under all and
api the loader mounts each core router once even if the manifest re-lists it, so
an extra never double-mounts a default.
The module list decides which modules load; a plugin’s own descriptor decides
where its routes mount. An installed router or channel plugin
declares its routes — a relative
base prefix and the rows under it — and the installer records both the module (in
routers_modules or channel_modules) and the chosen mount base. Every route
resolves at /api/ + base + its path, so listing the module never picks the URL:
the declaration does, and the operator can remap the base at
install. Your own router modules carry no
descriptor, so they mount their handlers’ absolute paths as written. A plugin route
that would collide with one the core or another plugin already owns is refused at
registration, never silently shadowed.
The Studio SPA catch-all matches every unclaimed path, so it must register last
or it shadows the routers after it. Under all the loader forces it last, and an
installed plugin router is inserted before it. Under none, where you list routers
yourself, keep the SPA catch-all module last in routers_modules if you serve the
Studio UI.
Tool extensions still load per module — you opt each one in by naming its module,
exactly like your own code. The management tools are a further exception: they
project from the operations layer, curated by
api_tools, not loaded module by
module.Manifest
and ApiToolsConfig models and the CLI reference for
tai manifest.
