Skip to content
K

Plugin Architecture

Monday Morning uses a plugin system to extend the MCP server with new tools, settings, and UI contributions. Plugins are loaded at server startup and their tools become available to any AI agent connected to the MCP server.

The plugin system has three layers:

  1. Plugin Registry — Discovers plugin directories, validates exports, and calls register().
  2. Plugin Interface — The MondayMorningPlugin contract that every plugin implements.
  3. MCP Server — Merges plugin tools into the server’s tool list so agents can call them.

There are two kinds of plugins:

TypeLocationLoaded By
BundledShips with the app in mcp-servers/monday-morning/plugins/Always loaded
CommunityInstalled to ~/.monday-morning/plugins/{id}/Loaded if present

Bundled plugins are compiled alongside the MCP server. Community plugins are standalone pre-compiled JavaScript packages installed from a zip or dropped into the plugins folder.

Both types implement the same MondayMorningPlugin interface and go through the same loading pipeline. Bundled plugins take precedence on ID conflicts.

Every plugin must default-export an object implementing MondayMorningPlugin:

interface MondayMorningPlugin {
/** Unique plugin identifier, e.g., "github" */
id: string;
/** Human-readable name, e.g., "GitHub Integration" */
name: string;
/** Brief description */
description: string;
/** Semver version string */
version: string;
/** Classification: "integration" | "migration" | "export" */
category: PluginCategory;
/** Tier for future entitlement gating (defaults to "free") */
tier?: PluginTier;
/** MCP tools this plugin provides */
tools: PluginToolDefinition[];
/** Called during registration to initialize the plugin */
register(context: PluginContext): Promise<void>;
/** Declarative settings schema for auto-rendered settings UI */
settings?: PluginSettingsSchema;
/** UI category for desktop sidebar grouping */
uiCategory?: PluginUICategory;
/** Declarative UI contributions (slots, sidebar items, widgets, actions) */
ui?: PluginUIRegistration;
/** Called when the plugin is enabled by the user */
onEnable?(): Promise<void>;
/** Called when the plugin is disabled by the user */
onDisable?(): Promise<void>;
/** Called when the user switches to a different project */
onProjectSwitch?(projectPath: string): Promise<void>;
}
FieldTypePurpose
idstringUnique identifier used internally
namestringDisplay name in the desktop app
descriptionstringShown in plugin listings
versionstringSemver version for the plugin
categoryPluginCategoryOne of "integration", "migration", "export"
toolsPluginToolDefinition[]Array of MCP tools the plugin provides
registerfunctionAsync init function called at load time
FieldTypePurpose
tier"free" | "pro"Future entitlement gating (default: "free")
settingsPluginSettingsSchemaDeclarative settings for auto-rendered UI
uiCategoryPluginUICategorySidebar grouping in desktop app
uiPluginUIRegistrationSlots, sidebar items, widgets, actions
onEnablefunctionCalled when user enables the plugin
onDisablefunctionCalled when user disables the plugin
onProjectSwitchfunctionCalled when user switches projects

On server startup, the PluginRegistry scans two directories for subdirectories containing an index.ts or index.js file:

  1. Bundled plugins — compiled output in the MCP server’s plugins/ directory
  2. Community plugins~/.monday-morning/plugins/ for user-installed plugins
const registry = new PluginRegistry(pluginsDir);
await registry.discoverAndLoad();

Bundled plugins are discovered first. Community plugins are discovered second — if a community plugin has the same id as a bundled plugin, it is skipped.

After importing, the registry validates the default export:

  • id must be a non-empty string
  • name must be a non-empty string
  • description, version must be strings
  • category must be "integration", "migration", or "export"
  • tools must be an array
  • register must be a function
  • No duplicate id values across plugins

If validation fails, the plugin is skipped with a warning.

Tier gating is live. Before registering a plugin, the registry resolves whether the session is entitled to its declared tier:

  • "free" (or absent) tier plugins always pass.
  • "pro" plugins pass when the resolved license tier is pro or team, or — for a free personal tier — when the user is on a team (the team-member overlay, resolved over the network and persisted in a durable snapshot so the catalog stays stable across sessions).
  • If tier resolution fails with an error, gating fails open with a warning — a corrupted license file should not silently shrink the toolset.

An unentitled pro plugin is locked, not dropped: it is discovered and listed in the manifest (so the desktop app can show it with an upgrade prompt), but register() is never called and its tools never enter the MCP tool surface. If entitlement is gained mid-session (e.g. the user joins a team), locked plugins are promoted live and the server emits notifications/tools/list_changed. Revocation works the other way at invocation time: each plugin tool call re-checks entitlement (from a short TTL cache) and returns an upgrade message instead of executing when the session is no longer entitled — tools are not yanked out of the listing mid-session.

The registry calls register() with a PluginContext:

interface PluginContext {
/** Absolute path to the project root */
projectPath: string;
/** Path utility functions for resolving .mm/ directory structure */
paths: typeof paths;
/** Logger for plugin output */
logger: PluginLogger;
}

The logger writes to stderr (appropriate for MCP servers). Use it instead of console.log:

register: async (context) => {
context.logger.info("My plugin registered");
}

After all plugins load, the server calls registry.getTools() to collect every PluginToolDefinition and merges them into the MCP server’s tool list. Agents see plugin tools alongside built-in tools with no distinction.

After loading, the registry writes a manifest.json to ~/.monday-morning/. The desktop app reads this manifest to populate the Plugins settings section without needing a running MCP server. Each entry carries a locked flag — true for a discovered-but-unentitled pro plugin, which the app lists with an upgrade prompt:

{
"version": 2,
"generatedAt": "2026-03-31T10:00:00.000Z",
"plugins": [
{
"id": "github",
"name": "GitHub Integration",
"description": "Sync issues and link pull requests",
"version": "1.0.0",
"category": "integration",
"tier": "free",
"tools": [
{ "name": "mm_sync_github", "description": "..." },
{ "name": "mm_link_github_pr", "description": "..." }
],
"settings": { "credentials": ["..."] },
"uiCategory": "development",
"ui": { "icon": "...", "slots": ["..."] },
"locked": false
}
]
}

Community plugins can be reloaded without restarting the server. The mm_reload_plugins tool (opt-in workflow group, always dispatchable by name) rescans ~/.monday-morning/plugins/, loads new plugins, unloads plugins whose directories were removed, and rewrites the global manifest. Bundled plugins are never touched by a reload.

CategoryPurpose
integrationConnect external services (GitHub, Slack, etc.)
migrationImport data from other tools (Obsidian, Jira)
exportExport Monday Morning data to other formats

UI categories determine how plugins are grouped in the desktop app’s plugin listings:

UI CategoryExamples
communicationsSlack, email integrations
developmentGitHub, GitLab
project-managementJira, Trello, Linear
financialHarvest, invoicing
data-storageVector stores, local files
export-reportingPDF export, dashboards
TierBehavior
freeAlways loads and registers
proRequires a pro/team license or team membership. Otherwise the plugin is listed as locked: visible in the desktop app with an upgrade prompt, but never registered and its tools absent from the MCP tool surface. If tier resolution errors, gating fails open.

Entitlement is also re-checked live on every plugin tool call, so a mid-session revocation returns an upgrade message rather than executing the tool. See Entitlement Check above for the full mechanics.