Skip to content
K

Creating a Plugin

This tutorial walks through building a community plugin as a standalone project. By the end, you will have a working plugin that can be installed into any Monday Morning instance and discovered by AI agents.

  • Node.js 18+
  • Basic TypeScript knowledge
  • A working Monday Morning installation (to test with)

Community plugins are standalone JavaScript packages that implement the MondayMorningPlugin interface. They are:

  • Developed independently — in their own repo, not inside the Monday Morning monorepo
  • Pre-compiled to JavaScript — shipped as .js files, not TypeScript
  • Installed globally — to ~/.monday-morning/plugins/{your-plugin-id}/
  • Loaded at MCP server startup — alongside bundled plugins, with the same capabilities
  1. Create your plugin project

    Terminal window
    mkdir mm-plugin-hello-world
    cd mm-plugin-hello-world
    npm init -y
  2. Install dependencies

    Your plugin needs zod for input schema validation (the MCP server provides it at runtime, but you need it for development):

    Terminal window
    npm install zod
    npm install -D typescript @types/node
  3. Configure TypeScript

    Create tsconfig.json:

    {
    "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
    },
    "include": ["src/**/*"]
    }
  4. Create the plugin types

    Create src/plugin-types.ts with the interfaces your plugin needs. You can copy these from the Plugin Architecture page, or use this minimal version:

    src/plugin-types.ts
    import type { ZodType } from "zod";
    export type PluginCategory = "integration" | "migration" | "export";
    export type PluginTier = "free" | "pro";
    export type PluginUICategory =
    | "communications"
    | "development"
    | "project-management"
    | "financial"
    | "data-storage"
    | "export-reporting";
    export interface PluginToolDefinition {
    name: string;
    description: string;
    inputSchema: ZodType;
    handler: (args: unknown) => Promise<unknown>;
    }
    export interface PluginLogger {
    info(message: string, ...args: unknown[]): void;
    warn(message: string, ...args: unknown[]): void;
    error(message: string, ...args: unknown[]): void;
    }
    export interface PluginContext {
    projectPath: string;
    paths: Record<string, (...args: string[]) => string>;
    logger: PluginLogger;
    }
    export interface CredentialField {
    key: string;
    label: string;
    type: "text" | "password";
    required: boolean;
    helpUrl?: string;
    helpText?: string;
    scopes?: string[];
    }
    export interface ConfigField {
    key: string;
    label: string;
    type: "text" | "number" | "boolean" | "select" | "channel" | "repo-picker" | "board-picker";
    default?: string | number | boolean;
    options?: { label: string; value: string }[];
    description?: string;
    dependsOn?: string;
    placeholder?: string;
    }
    export interface PluginSettingsSchema {
    credentials?: CredentialField[];
    config?: ConfigField[];
    projectConfig?: ConfigField[];
    }
    export interface PluginAction {
    id: string;
    label: string;
    context: "command-palette" | "entity-context-menu";
    command: string;
    }
    export interface PluginUIRegistration {
    icon?: string;
    logo?: string;
    tagline?: string;
    actions?: PluginAction[];
    }
    export interface MondayMorningPlugin {
    id: string;
    name: string;
    description: string;
    version: string;
    category: PluginCategory;
    tier?: PluginTier;
    tools: PluginToolDefinition[];
    register(context: PluginContext): Promise<void>;
    settings?: PluginSettingsSchema;
    uiCategory?: PluginUICategory;
    ui?: PluginUIRegistration;
    onEnable?(): Promise<void>;
    onDisable?(): Promise<void>;
    onProjectSwitch?(projectPath: string): Promise<void>;
    }
  5. Write your plugin

    Create src/index.ts:

    src/index.ts
    import { z } from "zod";
    import type { MondayMorningPlugin, PluginContext } from "./plugin-types.js";
    let pluginContext: PluginContext | undefined;
    const GreetInputSchema = z.object({
    project_path: z.string().describe("Absolute path to the project root"),
    name: z.string().optional().describe("Name to greet (default: World)"),
    });
    const helloWorldPlugin: MondayMorningPlugin = {
    id: "hello-world",
    name: "Hello World",
    description: "A minimal example plugin that greets the user",
    version: "1.0.0",
    category: "integration",
    tier: "free",
    uiCategory: "development",
    ui: {
    icon: "👋",
    tagline: "A friendly greeting plugin",
    actions: [
    {
    id: "hello-greet",
    label: "Say Hello",
    context: "command-palette",
    command: "mm_hello_world",
    },
    ],
    },
    tools: [
    {
    name: "mm_hello_world",
    description: "Say hello — a minimal example tool",
    inputSchema: GreetInputSchema,
    handler: async (args: unknown) => {
    const input = GreetInputSchema.parse(args);
    const name = input.name ?? "World";
    pluginContext?.logger.info(`Greeting ${name}`);
    return {
    success: true,
    message: `Hello, ${name}! Project: ${input.project_path}`,
    };
    },
    },
    ],
    register: async (context: PluginContext): Promise<void> => {
    pluginContext = context;
    context.logger.info("Hello World plugin registered");
    },
    };
    export default helloWorldPlugin;

    Key points:

    • The default export must be a MondayMorningPlugin object.
    • The id must be unique across all plugins.
    • Always include project_path in your tool input schemas.
    • Use pluginContext.logger instead of console.log.
  6. Add a manifest.json

    Create manifest.json in the project root. This is required for the marketplace and the plugin loader:

    {
    "id": "hello-world",
    "name": "Hello World",
    "version": "1.0.0",
    "description": "A minimal example plugin that greets the user",
    "author": {
    "name": "Your Name",
    "github": "your-github-username"
    },
    "category": "integration",
    "uiCategory": "development",
    "icon": "👋",
    "keywords": ["hello", "example", "starter"],
    "minAppVersion": "1.0.0",
    "license": "MIT"
    }
  7. Build the plugin

    Add a build script to package.json:

    {
    "type": "module",
    "scripts": {
    "build": "tsc"
    }
    }

    Then compile:

    Terminal window
    npm run build

    This produces dist/index.js — the file the MCP server will load.

  8. Install locally for testing

    Copy the compiled output, manifest, package.json, and node_modules to the global plugins directory:

    Terminal window
    mkdir -p ~/.monday-morning/plugins/hello-world
    cp dist/index.js ~/.monday-morning/plugins/hello-world/
    cp manifest.json ~/.monday-morning/plugins/hello-world/
    cp package.json ~/.monday-morning/plugins/hello-world/
    cp -r node_modules ~/.monday-morning/plugins/hello-world/
  9. Verify the plugin loads

    Restart the MCP server (or restart the Monday Morning desktop app). Check the logs for:

    [plugin-registry] INFO: Loaded plugin "Hello World" (hello-world) v1.0.0

    The tool mm_hello_world is now available to any connected AI agent.

You can test the tool by asking an AI agent to call it:

“Call the mm_hello_world tool with name ‘Developer’”

The agent will receive:

{
"success": true,
"message": "Hello, Developer! Project: /Users/you/your-project"
}

A complete community plugin looks like this when installed:

~/.monday-morning/plugins/hello-world/
├── index.js # Compiled plugin entry point (required)
├── manifest.json # Plugin metadata (required)
├── package.json # Must include "type": "module" (required)
├── node_modules/ # Dependencies like zod (required)
│ └── zod/
├── plugin-types.js # Compiled type definitions
└── lib/ # Optional: additional modules
└── helpers.js

The minimum required files are:

  • index.js — default export implementing MondayMorningPlugin
  • manifest.json — plugin metadata
  • package.json — with "type": "module"
  • node_modules/zod — the Zod validation library

To give your plugin a settings UI in the desktop app, add a settings property:

const myPlugin: MondayMorningPlugin = {
// ... existing fields ...
settings: {
credentials: [
{
key: "api_token",
label: "API Token",
type: "password",
required: true,
helpUrl: "https://example.com/tokens",
helpText: "Generate a token from your account settings",
},
],
config: [
{
key: "default_name",
label: "Default Name",
type: "text",
default: "World",
description: "The default name to use when greeting",
},
],
},
};

See Settings & Credentials for the full settings schema reference.

Plugins can contribute to the desktop app UI declaratively:

const myPlugin: MondayMorningPlugin = {
// ... existing fields ...
ui: {
icon: "👋",
tagline: "A friendly greeting plugin",
actions: [
{
id: "hello-greet",
label: "Say Hello",
context: "command-palette",
command: "mm_hello_world",
},
],
},
};

This registers a “Say Hello” action in the command palette. See Plugin Architecture for all UI contribution types.

The built-in GitHub plugin is a good reference for a production plugin. It demonstrates:

  • Two tools: mm_sync_github and mm_link_github_pr
  • Credential settings: A personal access token with help URL and scope requirements
  • UI contributions: Card badges, command palette actions, and context menu items
  • Separate handler files: Logic split into lib/sync.ts and lib/link-pr.ts

You can browse the source at WeekendDevsOrg/monday-morning/mcp-servers/monday-morning/plugins/github.