Skip to main content

Build an extension

Extensions are how you add new capabilities to Rational AI without changing the platform itself. A single extension is a small package — a manifest plus, optionally, some code — that the platform installs and wires into the right place: a tool an agent can call, a messaging channel users can write to, a source your Knowledge bases sync from, or a workflow that processes incoming files. This guide explains the kinds of extensions you can build, the manifest that describes each one, and how to package and install them.

📖WHAT IS AN EXTENSION?

An extension is a package described by a single manifest.json. The manifest declares one or more providers — capabilities the platform installs. The same package can ship several providers at once, but most extensions declare just one. Installing an extension reads its manifest and registers whatever it provides; uninstalling reverses that.

🔌BUILDING VS. INSTALLING

This page is the authoring guide — how to create an extension. If you only need to install, configure, enable or disable extensions from the registry, see Settings → Extensions instead.

The four kinds of provider

What an extension can do is determined entirely by which provider arrays its manifest fills in. There are four:

ProviderManifest keyWhat it addsShips code?
MCP servermcpServerProvidersTools, prompts and resources an agent can call during a conversation (fetch a page, query a database, search the web).Local: yes. Remote: no.
ChannelchannelProvidersAn inbound/outbound messaging channel users talk to the assistant through (e.g. Telegram).No
SourcesourceProvidersA data source your Knowledge bases sync files from, with an optional retention window.Usually yes
WorkflowworkflowProvidersA processing workflow that transforms files as they enter a Knowledge base.Yes

A manifest always lists all four arrays; leave the ones you don't use empty ([]).

Anatomy of an extension

A code-bearing extension is a directory named <name>-<version> with a standard layout:

my-extension-1.0.0/
├── manifest.json # metadata + provider declarations (required)
├── icon.png # extension icon shown in the registry
├── <name>-server/ # MCP server source (TypeScript or Python)
├── dist/ # built server / scripts the manifest points at
└── make-tar.sh # builds the distributable .tar.bz2

A remote extension (an MCP server hosted elsewhere) ships no code at all — it is just the manifest.json.

Common manifest fields

Every manifest starts with the same identifying fields, regardless of which providers it declares:

FieldRequiredDescription
idyesA stable UUID for the extension. Generate it once (uuidgen) and keep it for the life of the extension.
nameIdentifieryesA short machine slug, e.g. time, github-remote. Matches the folder name prefix.
versionyesSemantic version, matching the folder suffix (1.0.0).
nameyesDisplay name shown in the registry.
descriptionnoA sentence or two describing what the extension does. Shown to the user before they install.
iconnoIcon for the extension. May be empty (the bundled icon.png is used), a remote URL, or an embedded data: URI.
linknoURL to upstream docs or the project homepage.

The remaining keys are the four provider arrays described below.

1. MCP server extensions

The most common kind. An MCP server exposes tools (and optionally prompts and resources) following the Model Context Protocol; agents call them during a conversation. There are two transports.

Local (stdio)

A local server runs as a process the platform spawns. This is the default — omit transportType and supply a command and arguments:

{
"id": "fcb66d0a-1557-4041-8f93-2adc1999608f",
"nameIdentifier": "time",
"version": "1.0.0",
"name": "Time",
"description": "Get the current time and convert between IANA timezones.",
"icon": "",
"link": "https://mcp.so/server/time/modelcontextprotocol",
"channelProviders": [],
"mcpServerProviders": [
{
"name": "Time-server",
"path": "",
"command": "uvx",
"arguments": ["mcp-server-time", "--local-timezone=Europe/Rome"],
"allowedSources": [],
"configuration": {}
}
]
}

Useful fields on a local mcpServerProviders entry:

FieldDescription
command / argumentsThe process to spawn and its arguments.
pathWorking directory the process is spawned in, relative to the install dir (e.g. dist).
environmentEnvironment variables for the process. Values support ${config.*} interpolation, so secrets come from the user's configuration.
installScript / uninstallScriptCommands run once when the extension is installed / removed (e.g. to fetch dependencies).
beforeCallScript / afterCallScriptCommands run around each tool call.
configurationThe user-facing config schema (see Configuration fields).

Remote (HTTP)

A remote server already runs somewhere else; the platform connects to it over HTTP instead of spawning anything. Set transportType to "http" and give a url and optional headers:

{
"id": "407134fe-a77a-4fe5-8d9e-5ab08c4a01ad",
"nameIdentifier": "github-remote",
"version": "1.0.0",
"name": "GitHub (Remote)",
"description": "GitHub's official remote MCP server.",
"channelProviders": [],
"mcpServerProviders": [
{
"name": "GitHub Remote MCP Server",
"transportType": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": { "Authorization": "Bearer ${accessToken}" },
"configuration": {
"accessToken": {
"title": "GitHub Personal Access Token",
"provider": { "kind": "value", "type": "{ \"type\": \"string\" }" },
"isRequired": true,
"isHidden": false
}
}
}
]
}

Both url and header values support ${config.*} interpolation, so per-user credentials (an access token, a base URL) are filled in from the configuration the user provides at install time.

🔌REMOTE EXTENSIONS SHIP NO CODE

A remote extension has no server to build, so it is distributed as a plain manifest.json — upload the file directly, no .tar.bz2. With no bundled icon.png, set the manifest's icon field to a URL or an embedded data: URI.

OAUTH IS NOT YET SUPPORTED

Remote providers authenticate with a static value (a token or key) carried in a header. Servers that require an interactive OAuth 2.1 / DCR flow can't yet be expressed in a manifest — use a token-based server where one is offered.

Agent skills

Agent skills are delivered as a specialised MCP server extension: a sandbox server (mcpServerProviders) that loads SKILL.md skills from the package and offers them to the model with progressive disclosure. You author the skills as Markdown with frontmatter and bundle them alongside the server — the manifest shape is the same local-stdio form shown above.

2. Channel extensions

A channel lets users reach the assistant through an external messaging platform. Declare it under channelProviders and pick a type (Telegram, RationalAI, OpenAIApi):

{
"channelProviders": [
{
"id": "4c0369fa-ac30-4851-993f-ada2965d5e0c",
"name": "Telegram chat service",
"description": "Exchange messages via Telegram infrastructure.",
"type": "Telegram",
"configuration": {
"token": {
"alias": "Bot Token",
"description": "The bot token issued by Telegram's BotFather.",
"type": "{\"type\": \"string\"}",
"isRequired": true,
"isHidden": false,
"defaultValue": ""
}
}
}
]
}

The channel's configuration follows the same field shape as everything else (see Configuration fields). Hidden, pre-filled fields — such as a webhook URL the platform owns — are marked isHidden: true.

3. Source extensions

A source provider becomes a selectable data source when you create or sync a Knowledge base. A source is program-driven: at sync time the platform runs the program declared in runtime, hands it the source configuration as JSON on stdin, and reads back a { "files": [...] } list to download.

{
"sourceProviders": [
{
"id": "c0b2f8d3-4e5a-4b7c-9d2e-3f6a8b0c1d22",
"name": "ArXiv Catchup",
"description": "Download arXiv PDFs matching a category and time window.",
"runtime": {
"path": "dist",
"command": "uv",
"arguments": ["run", "arxiv_catchup.py", "list"],
"protocol": "list-json-v1"
},
"expiry": { "mode": "window" },
"configuration": {
"category": {
"alias": "Category",
"description": "arXiv category to fetch, e.g. cs.AI.",
"type": "{\"type\":\"string\"}",
"isRequired": true,
"isHidden": false
}
}
}
]
}
FieldDescription
runtime.command / argumentsThe resolver program. It receives the source config on stdin and prints { "files": [...] } on stdout.
runtime.pathWorking directory for the program, relative to the install dir.
runtime.protocolContract version the worker expects (list-json-v1).
expiry.modewindow removes any previously-synced file the resolver no longer returns; none never deletes.
🔌THE RESOLVER ONLY LISTS — THE PLATFORM DOWNLOADS

Your program's job is to return which files to sync, not to fetch them. The worker downloads each returned file and applies the expiry policy, so a resolver that returns only items inside the configured window gets out-of-window files cleaned up for free.

4. Workflow extensions

🆕NEW

Workflow providers are a recent addition to the extension system. They let an extension ship a ready-made processing workflow that the platform installs and assigns to Knowledge files.

A workflow provider points at a script in the package; on install, the platform reads that script and registers it as a global processing workflow. Because the script follows the same stdin/stdout contract as any hand-written workflow, the existing worker runs it unchanged — no extra wiring.

{
"id": "565fde73-85e4-4dce-b3a0-19423b472bb9",
"nameIdentifier": "transcribe",
"version": "1.0.0",
"name": "RationalAI Transcribe Extension",
"description": "Transcribe audio or video files into Markdown.",
"channelProviders": [],
"mcpServerProviders": [],
"sourceProviders": [],
"workflowProviders": [
{
"id": "transcribe-media",
"name": "Transcribe Audio/Video",
"description": "Transcribe an audio or video file into a Markdown resource.",
"path": "dist",
"script": "transcribe_workflow.py",
"timeout": 30,
"isActive": true
}
]
}
FieldDescription
pathDirectory inside the package the script lives in (e.g. dist).
scriptFilename of the workflow script, relative to path.
timeoutPer-run timeout in minutes passed to the activity.
isActiveWhether the workflow is assignable immediately after install.

The script itself is an ordinary processing workflow — a process(document, options) function wired up with run(process), with its dependencies declared inline in PEP 723 format so the worker resolves them at run time:

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["rational-client", "faster-whisper"]
# ///
from rational_client.core import File, Knowledge
from rational_client.utils import run


def process(document: File, options: dict):
knowledge = Knowledge(document.knowledge_id)
resource = knowledge.create_resource(name=document.name, file_id=document.id)
markdown = transcribe(document.get_data()) # your logic here
return {str(resource.resource_id): markdown}


run(process)
⚠️EXTENSION WORKFLOWS ARE READ-ONLY

A workflow installed from an extension is read-only: it appears in Settings → Workflows and can be assigned to files, but it can't be edited or deleted from the UI — its owning extension manages it. Uninstalling the extension is how you remove it.

Configuration fields

Every provider that needs user input — credentials, endpoints, options — declares a configuration map. Each entry is a field the platform renders at install time. The fields are consistent across provider types:

FieldDescription
title / aliasThe label shown to the user. (title on MCP providers, alias on channels and sources.)
descriptionHelp text. Plain HTML links are allowed for "how to get this value" instructions.
typeA JSON Schema fragment encoded as a string, e.g. "{\"type\":\"string\"}" or "{\"type\":\"string\",\"enum\":[\"day\",\"week\"]}".
isRequiredWhether the user must supply a value.
isHiddentrue for values the platform fills in itself (e.g. a webhook URL) — they don't appear in the form.
defaultValuePre-filled value.
providerOn MCP providers only: where the value comes from. { "kind": "value", ... } means the user types it directly.

Reference a configured value elsewhere in the manifest with ${fieldName} — that's how the GitHub example above pulls accessToken into its Authorization header.

Package and install

A code-bearing extension is built into a distributable archive by the make-tar.sh script in its directory:

cd my-extension-1.0.0
./make-tar.sh

This installs dependencies, builds the server, and produces a .tar.bz2 containing the built files, icon.png and manifest.json. A remote extension skips this entirely — its manifest.json is the distributable.

Install the result from the Control Room: open Settings → Extensions, click Add, and upload the archive (or, for a remote extension, the manifest). Once installed, configure any required fields, then enable it. See Settings → Extensions for the full install, configure and management flow.

START FROM AN EXISTING EXTENSION

The fastest way to build a new extension is to copy one that already does something similar and adjust its manifest. Keep id unique (generate a fresh UUID), and keep nameIdentifier and version consistent with the folder name.