Extensions

Modern AI applications evolve too quickly for a fixed collection of hard-coded screens. New model providers, tools, modalities and workflows arrive continuously, and enterprises need to disable capabilities, replace UI and integrate their own systems without maintaining a fork.

AI Chat solves this with a shared extension architecture spanning the server and the browser. Everything above the hosting layer is an extension, including the core conversation UI.

Built-in extensions

Extension What it adds
app Threads, history, avatars and the core conversation UI
agents Agent Profiles and the Profile Manager
analytics Cost, token and activity reporting for admins
api_tools Discovery and invocation of the App's own ServiceStack APIs
computer Filesystem tools and run_bash - off by default
core_tools Utilities, math and code execution tools
credentials Username/password sign-in for the Chat UI
custom Your App's own UI + routes, served from chat/custom/**
gallery Browsable catalog of generated images and audio
gemini Gemini File Search stores for RAG
identity Sign-in using the host App's ASP.NET Identity users
katex Mathematical typesetting
mcp The built-in MCP Server at /chat/mcp
pdf PDF Studio at /chat/pdf
projects Per-user workspaces and directory boundaries
publish Sharing threads, projects and media - off by default
skills Skill management, search, install and authoring
system_prompts The system prompt library
tools The shared Tool Registry and tools panel
voice Voice input and transcription

What an extension can contribute

Each extension can register only what it needs:

  • ServiceStack routes and APIs, under /{RoutePrefix}/ext/{name}/
  • Model tools and tool groups
  • UI components and pages
  • Sidebar and toolbar actions
  • Chat request, tool, approval, status, response and error filters
  • Import maps and client dependencies
  • Per-user files and preferences
  • Database tables and background workers
  • Startup and shutdown lifecycle hooks

The entire UI is assembled from registered Vue components, so an extension can add a new component or deliberately replace an existing building block by registering the same component name.

Disabling extensions

services.AddPlugin(new ChatFeature {
    DisableExtensions = ["computer", "publish", "katex"],
});

DisableExtensions removes the capability from the server and the UI together - a disabled extension registers no routes, no tools and no components.

The same list can be set in App_Data/chat/llms.json, which is merged with the programmatic list:

{
  "disable_extensions": ["computer"]
}

An extension can also disable itself. gemini does so when no Gemini API key is configured, voice when neither ffmpeg nor Mistral is available, and pdf when the typst CLI isn't on PATH. InstalledExtensionNames reports what actually loaded:

var installed = feature.InstalledExtensionNames;   // ["app","agents","tools",...]

Configuring an extension

ChatFeature exposes each built-in extension as a typed property, so configuration reads as one object graph:

services.AddPlugin(new ChatFeature {
    Tools    = { EnableCodeExecution = true, EnableFilesystemTools = true },
    ApiTools = { IncludeTags = ["CoffeeShop"] },
    Mcp      = { ToolGroups = ["api_tools"] },
    Publish  = { Enabled = true },
});
Property Extension
SystemPrompts system_prompts
App app
Agents agents
Projects projects
Tools tools
CoreTools core_tools
Computer computer
Gallery gallery
Skills skills
Voice voice
Publish publish
Gemini gemini
Katex katex
Pdf pdf
Analytics analytics
ApiTools api_tools
Mcp mcp
IdentityUi identity
Credentials credentials
Custom custom

AssertExtension<T>() resolves any of them, including extensions you add yourself:

var bookings = feature.AssertExtension<BookingToolsExtension>();

Install order

Extensions install in the order they appear in ChatFeature.Extensions. The built-in list ends with CustomExtension, so your App's own extension sees everything the built-ins registered - letting it override a tool, replace a UI component or reconfigure a group after the fact.

Adding your own extension appends it before that final custom extension:

services.AddPlugin(new ChatFeature {
    Extensions = {
        new BookingToolsExtension(),
    },
});

To run before a built-in instead, replace the list outright or insert at an index in Setup.

Lifecycle

Hook When
Install(ctx) During plugin registration, after config + providers are created
LoadAsync(ctx, token) After all extensions install, run concurrently
ctx.RegisterSetupUserHandler First request from each user
ctx.RegisterShutdownHandler AppHost disposal

An Install that throws logs the failure and skips that extension rather than failing App startup. Shutdown handlers are independent - one throwing doesn't skip the rest.

Static assets

An extension with a chat/ext/{name}/ folder of embedded resources gets its files served automatically at /{RoutePrefix}/ext/{name}/{path}, and one containing an index.mjs is registered in the /ext list so the Chat UI dynamically imports it on load.

Nothing under chat/custom/** is ever overwritten by an upstream UI sync, which makes it the place to put your own UI. See Custom Extensions.