Skip to content

Plugins

Warning

Plugins execute arbitrary Python code in the same process as MindRoom. A malicious plugin has full access to your credentials, Matrix sessions, file system, and network. Only install plugins you trust and have reviewed.

MindRoom plugins extend agents with custom tools, hooks, and skills. A plugin is a directory with a mindroom.plugin.json manifest, one or more Python modules, and optionally skill directories. Plugins are loaded from paths listed under plugins: in config.yaml.

Plugin structure

A plugin is a directory containing mindroom.plugin.json:

my-plugin/
├── mindroom.plugin.json   # Required manifest
├── tools.py               # Tool factories (optional)
├── oauth.py               # OAuth providers (optional)
├── hooks.py               # Event hooks (optional)
└── skills/                # Skill directories (optional)
    └── my-skill/
        └── SKILL.md

A plugin must have at least one of tools_module, hooks_module, oauth_module, or skills. A tools-only plugin exposes callable functions to agents. A plugin with oauth_module registers OAuth providers whose state, callbacks, and scoped credential storage are handled by MindRoom core. A hooks-only plugin observes or transforms events without adding agent-facing tools. Many plugins combine both.

Manifest format

The manifest is a JSON file named mindroom.plugin.json at the plugin root:

{
  "name": "my-plugin",
  "tools_module": "tools.py",
  "oauth_module": "oauth.py",
  "hooks_module": "hooks.py",
  "skills": ["skills"]
}
Field Type Required Description
name string yes Plugin identifier. Must be lowercase ASCII letters, digits, -, and _ only (pattern: ^[a-z0-9_-]+$). Must be unique across all configured plugins. Invalid or duplicate names abort plugin loading entirely.
tools_module string no Relative path to the Python module containing @register_tool_with_metadata factories. Must exist on disk if declared.
oauth_module string no Relative path to the Python module containing register_oauth_providers(settings, runtime_paths). Must exist on disk if declared.
hooks_module string no Relative path to the Python module containing @hook-decorated functions. Must exist on disk if declared.
skills list of strings no Relative directories containing skill subdirectories (each with a SKILL.md). Each directory must exist on disk.

Unknown fields are silently ignored. Invalid, duplicate, or malformed manifests are configuration errors and stop all plugin loading. All declared module files and skill directories must exist on disk.

If hooks_module is omitted, MindRoom auto-scans tools_module for @hook-decorated functions. If both fields point at the same file, MindRoom imports it once and reuses it for both tool registration and hook discovery.

Configure plugins

Add plugin paths under plugins: in config.yaml:

plugins:
  - ./plugins/my-plugin
  - python:my_skill_pack
  - path: ./plugins/personal-context
    enabled: true
    settings:
      dawarich_url: http://dawarich.local
      api_key: secret
    hooks:
      enrich_with_location:
        priority: 20
      audit_messages:
        enabled: false

Entry formats

Plugin entries can be strings (path only) or objects (with options). Both forms can be mixed in the same list.

String entry — just the path:

plugins:
  - ./plugins/my-plugin

Object entry — path plus options:

plugins:
  - path: ./plugins/my-plugin
    enabled: true
    settings:
      api_key: secret
    hooks:
      my_hook:
        enabled: false
Field Type Default Description
path string required Plugin path (see resolution rules below)
enabled bool true Set to false to disable the plugin without removing it from the list
settings dict {} Free-form key-value config passed to the plugin at load time
hooks dict {} Per-hook overrides keyed by hook function name

Each hook override supports:

Field Type Default Description
enabled bool true Disable a specific hook without removing it from the plugin
priority int null Override the hook's default execution priority
timeout_ms int null Override the hook's default timeout

Path resolution

Paths are resolved in this order:

  1. Absolute paths — used as-is
  2. Relative paths — resolved relative to the directory containing config.yaml
  3. Python package specs — see below

Python package plugins

MindRoom can resolve plugins from installed Python packages:

plugins:
  - my_skill_pack
  - python:my_skill_pack
  - pkg:my_skill_pack:plugins/demo
  - module:my_skill_pack:plugins/demo

Rules:

  • A bare package name (no slashes, no . or .. prefix) is tried as a Python package first.
  • python:, pkg:, and module: are explicit prefixes that force package resolution.
  • :sub/path after the package name points to a subdirectory inside the package.

MindRoom resolves the package location via importlib and looks for mindroom.plugin.json in that directory.

Tools module

A tools module is a Python file that registers one or more tool factories using the @register_tool_with_metadata decorator. Each factory function returns a Toolkit class (not an instance). MindRoom instantiates the class when building agents.

OAuth providers

An OAuth module registers provider definitions without registering FastAPI routes. MindRoom core owns state generation, callback consumption, authenticated requester binding, scoped OAuth token writes, status checks, and disconnect handling. The provider module supplies only provider-specific details such as endpoint URLs, scopes, token credential service names, optional tool config service names, optional PKCE requirements, token parsing, optional claim validators, and display metadata.

Declare the module in the manifest:

{
  "name": "drive-plugin",
  "tools_module": "tools.py",
  "oauth_module": "oauth.py"
}

Then expose register_oauth_providers(settings, runtime_paths):

from __future__ import annotations

from mindroom.oauth import OAuthProvider


def register_oauth_providers(settings, runtime_paths):
    del runtime_paths
    return [
        OAuthProvider(
            id="acme_drive",
            display_name="Acme Drive",
            authorization_url="https://accounts.acme.example/oauth/authorize",
            token_url="https://accounts.acme.example/oauth/token",
            scopes=("files.read",),
            credential_service="acme_drive_oauth",
            tool_config_service="acme_drive",
            client_config_services=(
                settings.get("client_config_service", "acme_drive_oauth_client"),
            ),
            allowed_email_domains=tuple(settings.get("allowed_email_domains", [])),
            allowed_hosted_domains=tuple(settings.get("allowed_hosted_domains", [])),
        ),
    ]

OAuth provider IDs are exposed through /api/oauth/{provider}/connect, /api/oauth/{provider}/authorize, /api/oauth/{provider}/callback, /api/oauth/{provider}/status, and /api/oauth/{provider}/disconnect. Dashboard flows normally call connect and use the returned provider authorization URL. Conversation flows should show the browser-openable authorize URL, because that URL first authenticates the MindRoom user and then redirects to the external provider. Conversation-issued links include an opaque connect token so the callback can verify the requester before storing scoped credentials. The connect token is also bound to the runtime requester, and redemption fails unless the authenticated dashboard user resolves to that requester. The callback stores tokens under credential_service using the resolved requester and agent execution scope, including private user and user_agent scopes. If the tool also has editable dashboard settings, declare tool_config_service and store those settings separately through the normal credentials API. Set pkce_code_challenge_method="S256" when the upstream OAuth provider requires PKCE. MindRoom stores the verifier in pending state and passes it as the fifth argument to custom token_exchanger callbacks. For example, an Acme Drive provider can store OAuth tokens in acme_drive_oauth while the acme_drive tool settings document contains only options such as file-size limits or capability toggles. Tokens and client secrets must never be written to config.yaml, prompt files, logs, or tool responses.

OAuth-backed tools should set setup_type=SetupType.OAUTH and auth_provider="<provider_id>" in @register_tool_with_metadata. When credentials are missing, return a concise instruction containing a browser-openable URL built with mindroom.oauth.build_oauth_connect_instruction(provider, runtime_paths, worker_target=...). The user can complete OAuth and retry the same tool request.

Deployment restrictions belong in plugin settings. Use allowed_email_domains to restrict verified email claims by domain. Use allowed_hosted_domains when the provider supplies a verified hosted-domain claim. If a configured restriction cannot be checked from verified claims, MindRoom fails the callback closed and does not save credentials.

Minimal example

from __future__ import annotations

from typing import TYPE_CHECKING

from mindroom.tool_system.metadata import (
    SetupType,
    ToolCategory,
    ToolStatus,
    register_tool_with_metadata,
)

if TYPE_CHECKING:
    from agno.tools import Toolkit


@register_tool_with_metadata(
    name="greeter",
    display_name="Greeter",
    description="A simple greeting tool",
    category=ToolCategory.DEVELOPMENT,
    status=ToolStatus.AVAILABLE,
    setup_type=SetupType.NONE,
)
def greeter_tools() -> type[Toolkit]:
    from agno.tools import Toolkit

    class GreeterTools(Toolkit):
        def __init__(self) -> None:
            super().__init__(name="greeter", tools=[self.greet])

        def greet(self, name: str) -> str:
            """Greet someone by name."""
            return f"Hello, {name}!"

    return GreeterTools

After registering the plugin, assign the tool to agents in config.yaml:

plugins:
  - ./plugins/my-greeter

agents:
  assistant:
    tools:
      - greeter

Decorator fields

All @register_tool_with_metadata arguments are keyword-only.

Required fields:

Field Type Description
name string Tool identifier — used to reference the tool in config.yaml agent tools: lists
display_name string Human-readable name shown in the dashboard
description string Brief description of what the tool does
category ToolCategory Category for dashboard grouping (see values below)

ToolCategory values: COMMUNICATION, DEVELOPMENT, EMAIL, ENTERTAINMENT, INFORMATION, INTEGRATIONS, PRODUCTIVITY, RESEARCH, SMART_HOME, SOCIAL.

Optional fields:

Field Type Default Description
status ToolStatus AVAILABLE AVAILABLE or REQUIRES_CONFIG — controls whether the tool appears as ready or needs setup in the dashboard
setup_type SetupType NONE NONE, API_KEY, OAUTH, or SPECIAL — tells the dashboard what kind of setup flow to show
config_fields list of ConfigField None Describes constructor parameters configurable through the dashboard (see ConfigField)
dependencies list of strings None Python packages the tool requires (see Dependencies)
docs_url string None Link to external documentation
icon string None Icon name for the dashboard (e.g., "FaGoogle", "Home")
icon_color string None Tailwind color class for the icon (e.g., "text-blue-500")
helper_text string None Markdown help text shown in the dashboard setup panel
auth_provider string None OAuth provider identifier when using OAuth-based setup
managed_init_args tuple of ToolManagedInitArg () Declares which MindRoom-managed values the toolkit constructor expects (see Managed init args)
default_execution_target ToolExecutionTarget PRIMARY PRIMARY or WORKER — controls whether the tool runs on the primary agent or a sandbox worker

Dependencies

The dependencies field lists Python packages that the tool requires at runtime. MindRoom checks whether each package is importable before the tool is instantiated.

For built-in tools (those shipped inside src/mindroom/tools/), missing dependencies trigger automatic installation via uv sync or pip install using the matching optional extra from MindRoom's pyproject.toml.

For plugin tools, automatic installation does not apply — there is no matching optional extra in MindRoom's package metadata. If the listed dependencies are not already installed in the environment, MindRoom raises an ImportError with a message listing the missing packages. Plugin authors should document their dependencies in their README so users can install them manually:

pip install openviking-client aiohttp

Even though plugin dependencies are not auto-installed, listing them in the decorator is still useful: MindRoom surfaces a clear error at tool load time rather than failing with an obscure traceback when an agent tries to call the tool mid-conversation.

You can disable automatic dependency installation entirely (for built-in tools too) by setting the environment variable MINDROOM_NO_AUTO_INSTALL_TOOLS=1.

ConfigField

Each ConfigField describes one constructor parameter that can be configured through the dashboard or credentials store.

Field Type Default Description
name string required Constructor kwarg name (e.g., "api_key")
label string required Display label shown in the dashboard
type string "text" Input type: text, password, url, number, boolean, or select
required bool True Whether the field must be set before the tool can be used
default any None Default value when not configured
placeholder string None Placeholder text shown in the input
description string None Help text for the field
options list None For select type: list of {"label": "...", "value": "..."} dicts
validation dict None Optional validation rules (min, max, pattern, etc.)

Example — a tool that requires an API key:

from mindroom.tool_system.metadata import (
    ConfigField,
    SetupType,
    ToolCategory,
    ToolStatus,
    register_tool_with_metadata,
)

@register_tool_with_metadata(
    name="weather",
    display_name="Weather",
    description="Get current weather data",
    category=ToolCategory.INFORMATION,
    status=ToolStatus.REQUIRES_CONFIG,
    setup_type=SetupType.API_KEY,
    config_fields=[
        ConfigField(name="api_key", label="API Key", type="password"),
        ConfigField(
            name="units",
            label="Units",
            type="select",
            required=False,
            default="metric",
            options=[
                {"label": "Metric (°C)", "value": "metric"},
                {"label": "Imperial (°F)", "value": "imperial"},
            ],
        ),
    ],
)
def weather_tools() -> type[Toolkit]:
    ...

Managed init args

If your toolkit constructor needs MindRoom-managed runtime values, declare them with managed_init_args. MindRoom does not auto-detect constructor parameter names — undeclared managed args are not passed through.

Value Constructor kwarg Description
RUNTIME_PATHS runtime_paths Storage paths, environment values, and data directory access
CREDENTIALS_MANAGER credentials_manager Read and write the per-tool credentials store
WORKER_TARGET worker_target Resolved worker routing context (scope, execution identity, worker key)

Example:

from agno.tools import Toolkit
from mindroom.tool_system.metadata import ToolCategory, ToolManagedInitArg, register_tool_with_metadata


@register_tool_with_metadata(
    name="needs_runtime",
    display_name="Needs Runtime",
    description="Example tool that needs runtime paths",
    category=ToolCategory.DEVELOPMENT,
    managed_init_args=(ToolManagedInitArg.RUNTIME_PATHS,),
)
def needs_runtime_tools() -> type[Toolkit]:
    class NeedsRuntimeTools(Toolkit):
        def __init__(self, *, runtime_paths):
            self.runtime_paths = runtime_paths
            super().__init__(name="needs_runtime", tools=[])

    return NeedsRuntimeTools

MCP via plugins (advanced)

MindRoom supports native MCP servers in config.yaml — see MCP for the normal setup path. This plugin pattern is still useful when you want a custom wrapper around Agno MCPTools:

from agno.tools.mcp import MCPTools
from mindroom.tool_system.metadata import (
    SetupType,
    ToolCategory,
    ToolStatus,
    register_tool_with_metadata,
)


class FilesystemMCPTools(MCPTools):
    def __init__(self, **kwargs):
        super().__init__(
            command="npx -y @modelcontextprotocol/server-filesystem /path/to/dir",
            **kwargs,
        )


@register_tool_with_metadata(
    name="mcp_filesystem",
    display_name="MCP Filesystem",
    description="Tools from an MCP filesystem server",
    category=ToolCategory.DEVELOPMENT,
    status=ToolStatus.AVAILABLE,
    setup_type=SetupType.NONE,
)
def mcp_filesystem_tools():
    return FilesystemMCPTools

Reference the plugin and tool in config.yaml:

plugins:
  - ./plugins/mcp-filesystem

agents:
  assistant:
    tools:
      - mcp_filesystem

The factory function must return the toolkit class, not an instance. MCP toolkits are async; Agno's async agent runs (arun, aprint_response) handle MCP connect and disconnect automatically.

Plugin skills

List skill directories in the manifest skills array. Each listed directory is added to MindRoom's skill search roots. Skill subdirectories must contain a SKILL.md file with YAML frontmatter (name, description, requirements).

Hooks

Plugins can ship typed event hooks for message enrichment, response transformation, lifecycle observation, tool call gating, reactions, schedules, and custom events. See the Hooks page for full documentation including:

  • The @hook decorator and all parameters
  • The built-in events and their execution modes
  • The enrichment pipeline (message:enrich)
  • Custom events
  • Error handling without cooldowns or circuit breakers
  • Testing patterns

Live development (hot reload)

Plugins hot-reload automatically. When you edit any file inside a configured plugin directory, MindRoom notices the change on the next poll, waits out the debounce window, re-imports the plugin's modules in place, swaps the new hooks and tools into the live registry, and the next event invokes your new code. In practice the new code is usually live about 1-2 seconds after a save. No service restart and no agent session disruption.

How it works

  • A background watcher polls each configured plugin root every ~1s and debounces saves over a ~1s window.
  • On change, the synthetic plugin package subtree is evicted from sys.modules, load_plugins() re-runs, a fresh HookRegistry is built, and the live registry is swapped atomically.
  • Module-level asyncio.Task objects, and one-level containers like dict[..., Task], on the old module are best-effort cancelled before the swap.
  • The watcher ignores __pycache__/, *.pyc, *.pyo, editor swap files (*.swp, *~, .#*, *.tmp), and tool caches (.ruff_cache/, .mypy_cache/, .pytest_cache/).

Iterating on a plugin

# 1. Edit any file under your plugin
$EDITOR ~/.mindroom/plugins/my-plugin/hooks.py

# 2. Save. Watch the journal:
journalctl -u mindroom.service -f | grep -E 'Reloading plugins|Plugin reload complete'

# 3. Trigger your hook (send a message, fire the matching event, etc.)
#    The new code path is live.

You can break and fix a plugin freely. A broken save that prevents reload, such as an import error or plugin validation error, can deactivate the affected plugin set, and the next valid save reloads it successfully. A hook that only raises at runtime is different: that failure is logged for that event, and the hook is tried again on the next matching event. There is no quarantine, failure threshold, or cooldown. Each save just reloads.

Manual reload

If you need to force a reload, for example because the watcher missed something or you want to confirm a swap explicitly, an admin user can send the chat command:

!reload-plugins

The bot replies with the active plugin set and the count of cancelled background tasks. Admin gating uses authorization.global_users from config.yaml.

Caveats and tradeoffs

The hot-reload path is intentionally best-effort, not transactional.

  • In-flight turns keep their old code. A reload swaps the registry for new events, but any callback already running on the old module finishes there, and only new events use the new module.
  • No partial-write detection. If your editor saves the file in two writes, the watcher may briefly load the half-written first state, log an import error, and then reload again on the second write.
  • CPU-bound infinite loops still wedge the event loop. The hook dispatcher uses asyncio.timeout() for cooperative cancellation, so truly blocking CPU code is not preempted.
  • Background resources held by the old module can leak until natural cleanup. Only asyncio.Task objects directly attached to module globals are cancelled, so plugins that hold long-lived non-task resources need their own cleanup bookkeeping.
  • New plugins added to disk are not auto-enabled. You still have to add them under plugins: in config.yaml, because the watcher only reloads plugins that are already configured.

Production tip

Hot reload is enabled by default in production. Edit any configured plugin directory directly while mindroom.service is running. ~/.mindroom/plugins/<name>/ is the common local layout, and active agent sessions, in-flight conversations, and streaming responses continue untouched.

Community plugins

The mindroom-ai organization maintains a collection of open-source plugins. Clone any of them into your plugins directory and add the path to config.yaml:

git clone https://github.com/mindroom-ai/ping-hook-plugin.git ~/.mindroom/plugins/ping-hook
plugins:
  - ~/.mindroom/plugins/ping-hook

Hooks-only plugins

Plugin Description
ping-hook-plugin Minimal example — responds to !ping-hook with a pong message. Good starting point for learning the hook system.
shell-guard-plugin Blocks dangerous shell commands (e.g., systemctl restart mindroom) via tool:before_call gating.
voice-enrich-plugin Injects AI-only metadata when a voice-transcribed message arrives, warning the model about possible transcription errors.
location-enrich-plugin Enriches prompts with real-time GPS location from Dawarich, including place matching and movement classification.
restart-resume-plugin Re-activates threads tagged pending-restart after a bot restart.

Hooks + tools plugins

Plugin Description
thread-snooze-plugin Snooze and unsnooze threads — temporarily resolves a thread and wakes it at a specified time.
thread-goal-plugin Persistent per-thread goals stored in Matrix room state that survive context compaction and restarts.
workloop-plugin Autonomous work plans with dependencies, priorities, auto-poke, and template-driven task creation.
openviking-plugin Long-term memory via OpenViking — automatic memory extraction, recall, and compaction archiving.