Settings & Credentials
Plugins can declare a settings schema that the desktop app uses to auto-render a configuration UI. This eliminates the need for custom Svelte components — you describe what settings you need, and the app builds the form.
PluginSettingsSchema
Section titled “PluginSettingsSchema”The settings property on MondayMorningPlugin accepts a PluginSettingsSchema:
interface PluginSettingsSchema { /** Credential fields (API keys, tokens, passwords) */ credentials?: CredentialField[]; /** Global configuration fields (apply to all projects) */ config?: ConfigField[]; /** Per-project configuration fields */ projectConfig?: ConfigField[]; /** OAuth configuration for browser-based authorization flow */ oauth?: OAuthConfig; /** Optional connection health check ("Test" button) */ healthCheck?: PluginHealthCheck;}There are three levels of settings:
| Level | Scope |
|---|---|
credentials | Global (all projects) |
config | Global (all projects) |
projectConfig | Per-project (scoped to the active project) |
In the desktop app, this schema is rendered in two settings sections. The Plugins section gives each installed plugin an inline Configure control that renders the full form — credentials, global config, and per-project config for the active project. The Integrations section shows per-project connection state: GitHub, Trello, and Harvest get dedicated inline connect rows, and every other enabled plugin with a settings schema appears with a Connected / Not connected status derived from whether its declared credential keys are present.
Credential Fields
Section titled “Credential Fields”Use CredentialField for API tokens, passwords, and secrets. The desktop app renders these with masked input fields and optional help links.
interface CredentialField { /** Storage key, e.g., "github_token" */ key: string; /** Display label */ label: string; /** Input type: "text" for visible, "password" for masked */ type: "text" | "password"; /** Whether the credential is required for the plugin to function */ required: boolean; /** URL to help the user generate/find this credential */ helpUrl?: string; /** Explanatory text shown below the input */ helpText?: string; /** OAuth scopes or permissions needed */ scopes?: string[]; /** Endpoint to test the credential against */ testEndpoint?: string;}Example: API Token
Section titled “Example: API Token”settings: { credentials: [ { key: "github_token", label: "Personal Access Token", type: "password", required: true, helpUrl: "https://github.com/settings/tokens/new", helpText: "Generate a PAT with 'repo' and 'read:org' scopes", scopes: ["repo", "read:org"], }, ],},The desktop app renders this as:
- A masked password input for the token value
- A clickable help link that opens the GitHub token creation page
- A description showing the required scopes
- A “Test Connection” button if
testEndpointis provided
Multiple Credentials
Section titled “Multiple Credentials”Some integrations need more than one credential:
settings: { credentials: [ { key: "api_key", label: "API Key", type: "password", required: true, helpUrl: "https://example.com/api-keys", helpText: "Find your API key in Account Settings", }, { key: "workspace_id", label: "Workspace ID", type: "text", required: true, helpText: "The numeric workspace ID from your dashboard URL", }, ],},Configuration Fields
Section titled “Configuration Fields”Use ConfigField for non-secret settings like feature toggles, default values, and picker fields.
interface ConfigField { /** Storage key */ key: string; /** Display label */ label: string; /** Input type */ type: "text" | "number" | "boolean" | "select" | "channel" | "repo-picker" | "board-picker"; /** Default value */ default?: string | number | boolean; /** Options for "select" type */ options?: { label: string; value: string }[]; /** Description shown below the input */ description?: string; /** Only show this field when another field has a truthy value */ dependsOn?: string; /** Placeholder text for text/number inputs */ placeholder?: string; /** Tauri command to fetch options for picker types */ fetchCommand?: string;}Field Types
Section titled “Field Types”Text Input
Section titled “Text Input”{ key: "default_label", label: "Default Label", type: "text", default: "bug", placeholder: "Enter a label name", description: "Applied to new issues by default",}Number Input
Section titled “Number Input”{ key: "sync_interval", label: "Sync Interval (minutes)", type: "number", default: 30, description: "How often to check for changes",}Boolean Toggle
Section titled “Boolean Toggle”{ key: "auto_sync", label: "Auto-sync on project open", type: "boolean", default: true, description: "Automatically sync when switching to this project",}Select Dropdown
Section titled “Select Dropdown”{ key: "sync_direction", label: "Sync Direction", type: "select", default: "both", options: [ { label: "Pull only", value: "pull" }, { label: "Push only", value: "push" }, { label: "Bidirectional", value: "both" }, ],}Picker Fields
Section titled “Picker Fields”Picker types (channel, repo-picker, board-picker) fetch their options dynamically from a Tauri command:
{ key: "github_repo", label: "Repository", type: "repo-picker", description: "Select the GitHub repository for this project", fetchCommand: "list_github_repos",}Conditional Fields
Section titled “Conditional Fields”Use dependsOn to show a field only when another setting is enabled:
config: [ { key: "notifications_enabled", label: "Enable Notifications", type: "boolean", default: false, }, { key: "notification_channel", label: "Notification Channel", type: "channel", dependsOn: "notifications_enabled", description: "Only shown when notifications are enabled", },],Per-Project Configuration
Section titled “Per-Project Configuration”Use projectConfig for settings that vary between projects. These appear in the project settings panel rather than global plugin settings:
settings: { credentials: [ { key: "api_token", label: "API Token", type: "password", required: true, }, ], projectConfig: [ { key: "board_id", label: "Board", type: "board-picker", description: "Select the board to sync with this project", fetchCommand: "list_boards", }, { key: "sync_labels", label: "Sync Labels", type: "boolean", default: true, description: "Include labels when syncing issues", }, ],},OAuth and Health Checks
Section titled “OAuth and Health Checks”Two optional schema fields upgrade the rendered form:
oauth(OAuthConfig) — declares a browser-based authorization flow. The app renders a “Connect with {provider}” button (e.g.connectLabel: "Connect with Slack"), optionally preceded by a one-time OAuth-app setup form forsetupCredentialslikeclient_id/client_secret. SethideManualEntry: trueto suppress the paste-a-token fallback when the credential cannot meaningfully be entered by hand.healthCheck(PluginHealthCheck) — names a Tauri command (e.g.test_trello_connection) the app invokes to verify that saved credentials actually work against the upstream API. When present, the form shows a Test control next to the Connected badge; a thrown error’s message is surfaced in the UI.
Credential Storage
Section titled “Credential Storage”Credentials entered through the settings UI are stored by the desktop app:
- Credential values are saved to the system credential store (the Keychain on macOS) rather than in app config files.
- For plugins whose MCP tools need the credential at call time, the app also writes a per-project bridge file at
.mm/integrations/{plugin-id}.json(e.g. Slack’s bot token, GitHub’s PAT)..mm/is gitignored by default, so bridge files stay local — but they are plain JSON on disk, which is worth knowing when you decide what to store there. - Some plugins also support environment-variable fallbacks (e.g.
GITHUB_TOKEN), resolved after the bridge file.
Accessing Credentials in Tool Handlers
Section titled “Accessing Credentials in Tool Handlers”Tool handlers can read credential values from the integration config files that the desktop app writes:
import * as fs from "fs/promises";import * as path from "path";
export async function myToolHandler(input: MyToolInput): Promise<MyToolOutput> { // Read integration config written by the desktop app const configPath = path.join(input.project_path, ".mm", "integrations", "my-plugin.json"); try { const raw = await fs.readFile(configPath, "utf-8"); const config = JSON.parse(raw); const token = config.api_token;
if (!token) { return { success: false, error: "API token not configured. Set it in Settings > Plugins.", }; }
// Use the token... } catch { return { success: false, error: "Plugin not configured. Set credentials in Settings > Plugins.", }; }}Complete Example
Section titled “Complete Example”Here is a full plugin with credentials, global config, and per-project config:
const slackPlugin: MondayMorningPlugin = { id: "slack", name: "Slack Integration", description: "Post updates and digests to Slack channels", version: "1.0.0", category: "integration", uiCategory: "communications",
settings: { credentials: [ { key: "slack_bot_token", label: "Bot Token", type: "password", required: true, helpUrl: "https://api.slack.com/apps", helpText: "Create a Slack app and install it to your workspace", scopes: ["chat:write", "channels:read"], }, ], config: [ { key: "digest_enabled", label: "Daily Digest", type: "boolean", default: false, description: "Post a daily summary to the configured channel", }, { key: "digest_time", label: "Digest Time", type: "select", default: "09:00", dependsOn: "digest_enabled", options: [ { label: "8:00 AM", value: "08:00" }, { label: "9:00 AM", value: "09:00" }, { label: "10:00 AM", value: "10:00" }, ], }, ], projectConfig: [ { key: "slack_channel", label: "Channel", type: "channel", description: "Slack channel for this project's updates", fetchCommand: "list_slack_channels", }, ], },
tools: [ // ... tool definitions ],
register: async (context) => { context.logger.info("Slack plugin registered"); },};Next Steps
Section titled “Next Steps”- Tool Registration — Define MCP tools with Zod schemas and handlers.
- Plugin Architecture — Understand the full plugin lifecycle.