Tool Registration
Every plugin provides one or more MCP tools via the tools array. Each tool is defined with a PluginToolDefinition that specifies its name, description, input schema, and handler function.
PluginToolDefinition
Section titled “PluginToolDefinition”interface PluginToolDefinition { /** Tool name, e.g., "mm_migrate_obsidian" */ name: string; /** Human-readable description of what the tool does */ description: string; /** Zod schema for validating tool input */ inputSchema: z.ZodType; /** Handler function that executes the tool logic */ handler: (args: unknown) => Promise<unknown>;}Tool Naming Convention
Section titled “Tool Naming Convention”All Monday Morning tools use the mm_ prefix:
| Pattern | Example | Purpose |
|---|---|---|
mm_<verb>_<noun> | mm_sync_github | Action on a resource |
mm_<plugin>_<action> | mm_link_github_pr | Plugin-specific action |
mm_<noun> | mm_hello_world | Simple single-purpose tools |
Input Schemas with Zod
Section titled “Input Schemas with Zod”Input schemas use Zod for runtime validation. The schema is used both for type inference and for generating the JSON Schema that AI agents see.
Basic Schema
Section titled “Basic Schema”import { z } from "zod";
export const MyToolInputSchema = z.object({ project_path: z.string().describe("Absolute path to the project root"), query: z.string().describe("Search query string"), limit: z.number().optional().default(10).describe("Maximum results to return"),});
export type MyToolInput = z.infer<typeof MyToolInputSchema>;Common Field Patterns
Section titled “Common Field Patterns”Most tools should include project_path as the first field:
// Required project path — almost every tool needs thisproject_path: z.string().describe("Absolute path to the project root"),Enum fields for constrained choices:
direction: z .enum(["pull", "push", "both"]) .optional() .describe("Sync direction (default: both)"),Optional fields with defaults:
format: z .enum(["markdown", "json"]) .optional() .default("markdown") .describe("Output format"),Boolean flags:
dry_run: z .boolean() .optional() .default(false) .describe("Preview changes without applying them"),Schema Best Practices
Section titled “Schema Best Practices”-
Always add
.describe()— The description string is shown to AI agents. Be specific about what the field does and what values are valid. -
Use
z.enum()for fixed choices — This gives agents a clear set of options instead of a free-text field. -
Provide defaults for optional fields — Agents can then call your tool with minimal arguments.
-
Include
project_path— The MCP server passes the active project path to tool calls. Most tools need it to locate the.mm/directory.
Handler Functions
Section titled “Handler Functions”The handler receives the raw arguments from the MCP server, validates them against the schema, and returns a result:
{ name: "mm_my_tool", description: "Does something useful", inputSchema: MyToolInputSchema, handler: async (args: unknown) => { // 1. Parse and validate input const input = MyToolInputSchema.parse(args);
// 2. Execute business logic const result = await doSomething(input);
// 3. Return structured result return result; },}Handler Pattern: Separate Logic
Section titled “Handler Pattern: Separate Logic”Keep handler functions thin. Put business logic in separate files under lib/:
import { doSomething, DoSomethingInputSchema } from "./lib/do-something.js";
const myPlugin: MondayMorningPlugin = { // ... tools: [ { name: "mm_do_something", description: "Does something useful", inputSchema: DoSomethingInputSchema, handler: async (args: unknown) => { const input = DoSomethingInputSchema.parse(args); return doSomething(input); }, }, ],};import { z } from "zod";import * as fs from "fs/promises";import * as path from "path";
export const DoSomethingInputSchema = z.object({ project_path: z.string().describe("Absolute path to the project root"), target: z.string().describe("What to operate on"),});
export type DoSomethingInput = z.infer<typeof DoSomethingInputSchema>;
export interface DoSomethingOutput { success: boolean; message: string;}
export async function doSomething( input: DoSomethingInput): Promise<DoSomethingOutput> { const mmDir = path.join(input.project_path, ".mm"); // ... implementation return { success: true, message: "Done" };}Handler Pattern: Using Plugin Context
Section titled “Handler Pattern: Using Plugin Context”If your handler needs the logger or path utilities from registration, capture the context:
let pluginContext: PluginContext | undefined;
const myPlugin: MondayMorningPlugin = { // ... register: async (context: PluginContext): Promise<void> => { pluginContext = context; },
tools: [ { name: "mm_my_tool", description: "Tool that uses plugin context", inputSchema: MyToolInputSchema, handler: async (args: unknown) => { const input = MyToolInputSchema.parse(args); pluginContext?.logger.info(`Running tool with: ${input.target}`); // ... logic return { success: true }; }, }, ],};
export default myPlugin;
// Re-export for use by lib/ modulesexport function getPluginContext(): PluginContext | undefined { return pluginContext;}Return Values
Section titled “Return Values”Tool handlers should return structured objects. Common patterns:
// Success with datareturn { success: true, data: { items: [...], count: 42 },};
// Success with messagereturn { success: true, message: "Synced 5 issues from GitHub", pulled: 5, pushed: 0,};
// Errorreturn { success: false, error: "GitHub token not configured. Set it in Settings > Plugins > GitHub.",};Error Handling
Section titled “Error Handling”Zod’s .parse() throws a ZodError if validation fails. The MCP server catches this and returns a structured error to the agent. For business logic errors, return an error object instead of throwing:
handler: async (args: unknown) => { const input = MyToolInputSchema.parse(args);
try { const result = await riskyOperation(input); return { success: true, data: result }; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, error: message }; }},Multiple Tools Per Plugin
Section titled “Multiple Tools Per Plugin”A plugin can register any number of tools. Group related functionality into a single plugin:
const myPlugin: MondayMorningPlugin = { id: "jira", name: "Jira Integration", // ... tools: [ { name: "mm_sync_jira", description: "Sync issues between Jira and Monday Morning", inputSchema: SyncJiraInputSchema, handler: async (args) => { /* ... */ }, }, { name: "mm_link_jira_ticket", description: "Link a Jira ticket to a Monday Morning spec", inputSchema: LinkJiraTicketInputSchema, handler: async (args) => { /* ... */ }, }, { name: "mm_jira_status", description: "Check the sync status of a Jira project", inputSchema: JiraStatusInputSchema, handler: async (args) => { /* ... */ }, }, ],};Working with the .mm/ Directory
Section titled “Working with the .mm/ Directory”Community plugins don’t have access to the internal paths module. Use path.join() with project_path to build paths to the .mm/ directory:
import * as path from "path";import * as fs from "fs/promises";
// Get the .mm/ directory for a projectconst mmDir = path.join(projectPath, ".mm");
// Build paths to specific directoriesconst specsDir = path.join(projectPath, ".mm", "specs");const issuesDir = path.join(projectPath, ".mm", "issues");
// Check if a directory existsasync function exists(p: string): Promise<boolean> { try { await fs.access(p); return true; } catch { return false; }}Next Steps
Section titled “Next Steps”- Settings & Credentials — Add settings UI and credential storage to your plugin.
- Creating a Plugin — End-to-end tutorial building a minimal plugin.