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.

Plugins extend MindRoom with custom tools, hooks, OAuth providers, and skills without changing MindRoom itself. Use one to add an integration MindRoom does not ship, react to or transform events, or connect an additional Google or Atlassian account. A plugin is a directory with a mindroom.plugin.json manifest, loaded from the paths listed under plugins: in config.yaml.

Installing plugins

Vendor a plugin from GitHub into <config dir>/plugins/<repo name>:

mindroom plugins install ping-hook-plugin
mindroom plugins install mindroom-ai/ping-hook-plugin@v1.2.0

The spec is NAME, OWNER/REPO, or OWNER/REPO@REF. Bare names install from the mindroom-ai organization, and @REF pins a branch, tag, or commit; without it the repository's default branch is used. A plugin is installed only after it passes the compatibility check. The installed directory records the repository, requested reference, and exact commit in .mindroom-plugin.lock.json. Installing into an existing directory fails with Plugin directory already exists: ... Use 'mindroom plugins update' instead. Plugin Python dependencies are not installed; the command prints a note when the plugin has a pyproject.toml.

Then add the plugin to config.yaml:

plugins:
  - path: plugins/ping-hook-plugin

Update vendored plugins:

mindroom plugins update ping-hook-plugin
mindroom plugins update --all
mindroom plugins update ping-hook-plugin --ref v1.3.0

An update fetches the latest commit of the pinned reference, or repins with --ref, and skips plugins already at that commit. Installing a new commit replaces the whole plugin directory and discards local edits to it, so copy any changes you want to keep first. A failed update leaves the installed version untouched. Both commands accept --path to select the config file whose directory is used and --plugins-dir to override the vendor directory. Set GITHUB_TOKEN to authenticate GitHub requests, which raises API rate limits and allows private repositories.

Community plugins

The mindroom-ai organization maintains these open-source plugins.

Plugin Provides Description
ping-hook-plugin Hooks Minimal example that answers !ping-hook with a pong; a good starting point for learning hooks.
shell-guard-plugin Hooks Blocks dangerous shell commands, such as systemctl restart mindroom-chat, through tool:before_call gating.
voice-enrich-plugin Hooks Warns the model about possible transcription errors in voice-transcribed messages.
location-enrich-plugin Hooks Adds real-time GPS location from Dawarich to prompts, with place matching and movement classification.
restart-resume-plugin Hooks Re-activates threads tagged pending-restart after a bot restart.
thread-snooze-plugin Hooks and tools Temporarily resolves a thread and wakes it at a specified time.
thread-goal-plugin Hooks and tools Per-thread goals stored in Matrix room state that survive context compaction and restarts.
workloop-plugin Hooks and tools External workloop; not needed for per-thread todo plans and auto-poke, which MindRoom provides natively.
openviking-plugin Hooks and tools Long-term memory through OpenViking with automatic extraction, recall, and compaction archiving.

Configure plugins

List plugins under plugins: in config.yaml. An entry is either a path string or an object with options, and both forms can be mixed:

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

Field Type Default Description
path string required Plugin directory or Python package spec (see Path resolution)
enabled bool true Set to false to disable the plugin without removing the entry
settings dict {} Free-form values passed to the plugin's hooks and OAuth module; masked as secret in the dashboard
hooks dict {} Per-hook overrides keyed by hook function name

Each hook override supports:

Field Type Default Description
enabled bool true Set to false to disable that hook
priority int null Override the hook's execution priority
timeout_ms int null Override the hook's timeout in milliseconds

Adding, removing, or changing a plugins: entry takes effect through config hot reload.

Path resolution

  • Absolute paths and paths starting with ~ are used as-is.
  • Relative paths resolve against the directory containing config.yaml.
  • A bare name without / that does not start with . uses a matching config-relative directory if one exists, otherwise an importable Python package.

Python package plugins

Plugins can ship inside installed Python packages:

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

The python:, pkg:, and module: prefixes always resolve a package, and :sub/path after the package name points to a subdirectory inside it. The resolved directory must contain mindroom.plugin.json.

Live development (hot reload)

Plugins hot-reload automatically. Saving any file inside a configured plugin directory makes the new hooks and tools live for the next event, usually 1-2 seconds after the save. No service restart or agent session reset is needed. Edits to __pycache__/, *.pyc, *.pyo, editor swap and backup files (*.swp, *.swo, *~, .#*, *.tmp), .git/, and tool caches (.ruff_cache/, .mypy_cache/, .pytest_cache/) are ignored.

Important

Every plugin reload, automatic or manual, interrupts all unfinished background scripts, including scripts that do not use the changed plugin.

Watch reloads in the logs:

journalctl -u mindroom.service -f | grep -E 'Reloading plugins|Plugin reload complete'

Keep in mind:

  • Only plugins already listed under plugins: are watched; a new plugin directory on disk is not loaded until you add it to config.yaml.
  • Callbacks already running finish on the old code; the new code applies to new events.
  • An editor that saves a file in two writes can briefly trigger an import error, followed by a successful reload on the second write.
  • Reload cancels asyncio tasks the plugin keeps in module-level variables; close connections and other resources yourself.

Broken plugins

mindroom run startup does not crash on a broken plugin. It logs Failed to load plugin, skipping (or Plugin path does not exist, skipping) and disables that plugin's tools. If a broken tools module hides which tools it would have registered, unknown tool names in agent configs are also disabled with a warning. Two plugins whose manifests share a name stop the whole configured plugin set from loading. mindroom config validate and mindroom plugins check report the same problems as errors instead.

When a save breaks a plugin during hot reload, MindRoom logs Plugin reload failed; active plugin set degraded and keeps the other plugins active, or logs Plugin reload failed; all plugins deactivated when no valid set remains. The next valid save reloads normally. For hooks that raise while handling an event, see Errors and timeouts.

MindRoom logs Loading non-bundled plugin once for each plugin outside the MindRoom source tree; this is informational.

!reload-plugins

Force-reload every configured plugin from disk, for example when the watcher missed a change:

!reload-plugins

The reply lists the active plugins and the number of cancelled background tasks:

✅ Reloaded N plugins; cancelled K tasks; active: <plugin names>

Only users listed in administrators in config.yaml may run it; others get ❌ Admin only. A failed reload replies ❌ Plugin reload failed: <error>. !reload_plugins is an alias.

Compatibility checks

Validate a plugin against the installed MindRoom version before deployment:

mindroom plugins check ./my-plugin

The check strictly validates the manifest, imports declared modules, validates tool, hook, and OAuth registrations, and verifies that declared skill directories exist. It prints the discovered tools, hooks, and skill directories, and exits nonzero on failure. It does not parse SKILL.md contents or evaluate skill eligibility, and it does not touch your running configuration.

Plugin structure

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

Only the manifest name is required; a plugin may provide any combination of tools, hooks, OAuth providers, and skills.

Manifest format

{
  "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 using only lowercase ASCII letters, digits, -, and _; must be unique among configured plugins
tools_module string no Relative path to the module with @register_tool_with_metadata factories
oauth_module string no Relative path to the module defining register_oauth_providers(settings, runtime_paths)
hooks_module string no Relative path to the module with @hook functions; when omitted, tools_module is scanned for hooks
skills list of strings no Relative directories containing skill subdirectories

Every declared module file and skill directory must exist. Unknown fields are ignored. Pointing tools_module and hooks_module at the same file is allowed.

Tools module

A tools module registers tool factories with @register_tool_with_metadata. Each factory returns a Toolkit class, not an instance, and MindRoom constructs it when building agents.

Minimal example

from __future__ import annotations

from typing import TYPE_CHECKING

from mindroom.tool_system.declarations import (
    SetupType,
    ToolCategory,
    ToolFileAccess,
    ToolStatus,
)
from mindroom.tool_system.registration import register_tool_with_metadata

if TYPE_CHECKING:
    from agno.tools import Toolkit


@register_tool_with_metadata(
    name="greeter",
    file_access=ToolFileAccess.NONE,
    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

Assign the tool to agents like any built-in tool:

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 in agent tools: lists
display_name string Name shown in the dashboard
description string Brief description of the tool
category ToolCategory Dashboard grouping: COMMUNICATION, DEVELOPMENT, EMAIL, ENTERTAINMENT, INFORMATION, INTEGRATIONS, PRODUCTIVITY, RESEARCH, SMART_HOME, or SOCIAL
file_access ToolFileAccess How the tool reaches local files (see below)

ToolFileAccess values:

  • NONE: the tool takes no local file paths.
  • AGENT: the tool resolves every model-supplied path through mindroom.file_access.resolve_agent_file, so it follows the agent's file_access setting.
  • UNCONFINED: the tool reaches local files in a way file_access does not confine, such as running programs, queries, or model-chosen paths.

See Security Posture for how each class is treated.

Optional fields:

Field Type Default Description
status ToolStatus AVAILABLE AVAILABLE or REQUIRES_CONFIG; whether the dashboard shows the tool as ready or needing setup
setup_type SetupType NONE NONE, API_KEY, OAUTH, or SPECIAL; which setup flow the dashboard shows
config_fields list of ConfigField None Constructor parameters configurable through the dashboard (see ConfigField)
agent_override_fields list of ConfigField None Constructor parameters an agent can set in its own tools: entry but defaults.tools cannot
dependencies list of strings None Python packages the tool requires (see Dependencies)
docs_url string None Link to external documentation
icon string None Dashboard icon name, such as "FaGoogle" or "Home"
icon_color string None Tailwind color class for the icon, such as "text-blue-500"
helper_text string None Markdown help shown in the dashboard setup panel
auth_provider string None OAuth provider ID for OAuth-backed tools (see OAuth providers)
oauth_fallback_fields tuple of strings () config_fields names that let the dashboard offer manual credentials as an alternative to OAuth and count the tool as configured once they are all set; requires setup_type=OAUTH; list token-named fields such as access_token in the provider's tool_config_oauth_fallback_fields too
managed_init_args tuple of ToolManagedInitArg () MindRoom-managed values passed to the constructor (see Managed init args)
default_execution_target ToolExecutionTarget PRIMARY Default location, PRIMARY or WORKER; worker routing configuration can override it
requires_primary_runtime bool False Never route the tool to a worker, even when worker_tools lists it
consumes_workspace_paths bool False The tool opens workspace files by path; when it runs in a worker, attachments are saved to the worker workspace so the tool can open them
requires_room_context bool False The tool needs a live Matrix room, so it is hidden where none exists, such as the MCP gateway, the OpenAI-compatible API, and room-less runs, and it always runs in the primary runtime
executes_code bool False The tool runs arbitrary programs; MindRoom warns at startup when such a tool runs in a worker while the same agent keeps unconfined tools in the primary process
supports_toolkit_filters bool True Accept the include_tools and exclude_tools inline overrides
worker_inert_agent_functions tuple of strings () Functions whose injected agent parameter is unused and can receive None in a worker

Set requires_primary_runtime=True when the toolkit depends on primary-process services, keeps state or an open resource between calls, or reads or changes an injected Agent, Team, or RunContext. A worker builds a fresh toolkit for every call and accepts only JSON values and paths as arguments. If an SDK function accepts an agent parameter but never uses it, list it in worker_inert_agent_functions instead; never do this for functions that depend on agent identity, configuration, or session state. requires_room_context and requires_primary_runtime describe where a tool can run; they do not grant or replace tool authorization.

Dependencies

Plugin tool dependencies are never installed automatically. When a listed package is missing, the tool fails to load with Missing dependencies for tool '<name>': <packages> instead of failing mid-conversation. Document the dependencies in the plugin README so users can install them, for example pip install openviking-client aiohttp. Built-in tools install their own dependencies as described in Automatic Dependency Installation.

ConfigField

Each ConfigField describes one constructor parameter that users can set through the dashboard or credentials store.

Field Type Default Description
name string required Constructor keyword argument, such as "api_key"
label string required Label shown in the dashboard
type string "text" text, password, url, number, boolean, select, or string[]
required bool True Whether the field must be set before the tool can be used
default any None Initial value in the dashboard form
placeholder string None Placeholder text in the input
description string None Help text for the field
options list None For select: a list of {"label": "...", "value": "..."} dicts
validation dict None Validation rules such as min, max, or pattern
authored_override bool True Set to False to forbid setting the field inline in config.yaml; password fields are never allowed inline (see Per-Agent Tool Configuration)

A ConfigField default only pre-fills the dashboard form and is not passed to the constructor. When no value is stored or configured, the constructor's Python default applies, so give optional parameters matching defaults, such as units: str = "metric" below.

from mindroom.tool_system.declarations import (
    ConfigField,
    SetupType,
    ToolCategory,
    ToolFileAccess,
    ToolStatus,
)
from mindroom.tool_system.registration import register_tool_with_metadata

@register_tool_with_metadata(
    name="weather",
    file_access=ToolFileAccess.NONE,
    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

Declare MindRoom-managed constructor values with managed_init_args. Only declared values are passed; MindRoom does not detect them from parameter names.

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
RUNTIME_CONFIG runtime_config The active MindRoom config, when available
AGENT_NAME agent_name The constructing agent's name, when available
FILE_ACCESS file_access The constructing agent's effective file_access setting
WORKER_TARGET worker_target Resolved worker routing context (scope, execution identity, worker key)
TOOL_OUTPUT_WORKSPACE_ROOT tool_output_workspace_root Workspace root used for managed tool-output saves
WORKER_TOOLS_OVERRIDE worker_tools_override Effective worker-routed tool override
CURRENT_ROOM_ID current_room_id Active Matrix room ID when available
AGENT_STATE_ROOT agent_state_root The constructing agent's state root in the primary runtime, requester-scoped for private agents; None inside workers
from agno.tools import Toolkit
from mindroom.tool_system.declarations import ToolCategory, ToolFileAccess, ToolManagedInitArg
from mindroom.tool_system.registration import register_tool_with_metadata


@register_tool_with_metadata(
    name="needs_runtime",
    file_access=ToolFileAccess.NONE,
    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

Tool runtime context

Inside a Matrix-connected agent run, mindroom.tool_system.runtime_context.get_tool_runtime_context() returns the current ToolRuntimeContext, or None outside one. It carries room_id, thread_id, resolved_thread_id, requester_id, agent_name, the Matrix client, the active config, and runtime paths. thread_id is the thread the inbound message named, while resolved_thread_id is the conversation thread after plain replies are resolved to their thread.

Dispatch-time worker targets

A tool that builds other registered tools while handling a call, for example a sub-toolkit from get_tool_by_name, should use the same worker target as agent toolkit construction so scoped credentials and OAuth MCP sessions resolve identically:

from mindroom.tool_system.runtime_context import get_tool_runtime_context

context = get_tool_runtime_context()
worker_target = context.resolve_worker_target()

resolve_worker_target() raises ValueError in team and router dispatches, which have no single agent scope; catch it if the tool can run inside a team.

OAuth providers

An OAuth module defines providers, and MindRoom core handles the connect, callback, credential storage, status, and disconnect flows described in the OAuth framework. Declare the module in the manifest:

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

Then define register_oauth_providers(settings, runtime_paths), which receives the plugin entry's settings:

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", [])),
        ),
    ]

The OAuth framework owns the service naming rules and the PKCE option.

  • tool_config_service is optional and holds the tool's editable dashboard settings, such as size limits or capability toggles, separately from the tokens.
  • allowed_email_domains and allowed_hosted_domains restrict which accounts may connect; read them from plugin settings so each deployment sets its own.

Never write tokens or client secrets to config.yaml, prompt files, logs, or tool responses.

Providers that publish protected-resource metadata can discover their endpoints and optionally register a client automatically:

from mindroom.oauth import OAuthDiscoveryConfig, OAuthProvider, oauth_runtime_bootstrapper

provider = OAuthProvider(
    id="acme_drive",
    display_name="Acme Drive",
    authorization_url="",
    token_url="",
    scopes=("files.read",),
    credential_service="acme_drive_oauth",
    client_config_services=("acme_drive_oauth_client",),
    token_endpoint_auth_method="none",
    pkce_code_challenge_method="S256",
    runtime_bootstrapper=oauth_runtime_bootstrapper(
        OAuthDiscoveryConfig(
            resource="https://api.acme.example",
            token_endpoint_auth_method="none",
            pkce_code_challenge_method="S256",
        ),
    ),
)

OAuth-backed tools set setup_type=SetupType.OAUTH and auth_provider="<provider_id>". When credentials are missing, return an instruction with a browser-openable connect link so the user can connect and retry the request. Build the link with oauth_connect_url(provider, runtime_paths, worker_target=...) and the message with build_oauth_connect_instruction(provider, connect_url), both from mindroom.oauth.service.

Additional Google workspaces

Use GoogleWorkspaceConfig to add another Google account alongside the built-in Google tools. Each workspace gets its own OAuth providers, stored connections, Connections page cards, and prefixed tool and function names, while reusing the built-in Google tool implementations.

One module can register both the tools and the OAuth providers:

{
  "name": "google-workspaces",
  "tools_module": "workspaces.py",
  "oauth_module": "workspaces.py"
}
# workspaces.py
from mindroom.tool_system.google_workspaces import (
    GoogleWorkspaceConfig,
    google_workspace_oauth_providers,
    register_google_workspace_tools,
)

WORKSPACES = (
    GoogleWorkspaceConfig(
        name="secondary",
        display_name="Secondary",
        client_config_service="secondary_google_oauth_client",
        allowed_hosted_domains=("secondary.example",),
        services=("gmail",),
    ),
)

for workspace in WORKSPACES:
    register_google_workspace_tools(workspace)


def register_oauth_providers(settings, runtime_paths):
    return tuple(
        provider
        for workspace in WORKSPACES
        for provider in google_workspace_oauth_providers(workspace)
    )
Field Description
name Prefix for tool, provider, and function names: a lowercase letter followed by up to 15 lowercase letters, digits, or _; keep it stable because it identifies stored connections
display_name Label prefix in the dashboard, such as Secondary Gmail
client_config_service Credential service holding the workspace's OAuth client; must end with _oauth_client
allowed_hosted_domains At least one Google hosted domain; signing in with a personal account or another organization's account fails without saving credentials
services Any of gmail, google_calendar, google_drive, google_docs, google_sheets, and google_tasks, without duplicates; defaults to all six

To set up a workspace:

  1. Provision its OAuth client through a shared credential seed with client_id and client_secret under the client_config_service.
  2. For each service, enable the matching Google API and register its callback on that client, such as https://your-host/api/oauth/secondary_google_gmail/callback.
  3. Enable the plugin and add the prefixed tools, such as secondary_gmail beside gmail, to the agent's tools.

When a workspace tool needs a login, it returns its own chat authorization link. Workspace tools never fall back to the default Google client or a global service account, and like the built-in Google tools they always run in the primary runtime. Functions are prefixed, such as secondary_get_latest_emails and secondary_send_email, so approval rules, script-tool allowlists, and function filters must use those names; a rule for send_email does not match secondary_send_email.

Additional Atlassian connections

Use AtlassianConnectionConfig the same way to add another Atlassian Cloud site alongside the built-in atlassian tool. See Atlassian Cloud for the plugin example, fields, and callback setup.

MCP via plugins (advanced)

Configure MCP servers directly in config.yaml as described in MCP. Use a plugin only when you need a custom wrapper around Agno MCPTools:

from agno.tools.mcp import MCPTools
from mindroom.tool_system.declarations import (
    SetupType,
    ToolCategory,
    ToolFileAccess,
    ToolStatus,
)
from mindroom.tool_system.registration import 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",
    # The filesystem server reads local paths that file_access cannot confine.
    file_access=ToolFileAccess.UNCONFINED,
    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

Add the plugin and assign mcp_filesystem to agents as in the minimal example. MindRoom connects and disconnects the MCP session around each agent run.

Plugin skills

Each directory listed in the manifest skills array becomes a skill search root, and each skill subdirectory needs a SKILL.md. See Skills for the SKILL.md format, precedence, and the per-agent skills: allowlist that plugin skills also require.

Hooks

Plugins can ship typed event hooks for message enrichment, response transformation, lifecycle observation, tool-call gating, reactions, schedules, and custom events. See Hooks for the @hook decorator, events, execution modes, timeouts and errors, the hook context and Matrix helpers, and testing.