Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

miro

A Claude Code plugin that bundles a local Miro MCP server, giving Claude tools to create and manage boards, sticky notes, shapes, frames, connectors, and tags for EventStorming, brainstorming, and diagramming workflows.

This is the marketplace's first plugin to ship its own MCP server. The server is a single self-contained Node artifact (server/dist/index.min.js) invoked over local stdio, so enabling the plugin adds the Miro tools with no separate install, no registry token, and no npx dependency (a bundled node <server> sidesteps the Windows bare-npx spawn bug, anthropics/claude-code#58510).

Enabling and configuration

The plugin installs disabled (defaultEnabled: false). A bundled MCP server that connects to an external, credentialed service is opt-in, not on by default. Enable it with claude plugin enable miro or the /plugin interface. Set a token before first use:

Option Storage Purpose
miro_api_token Claude Code secure credential storage (never settings.json) Miro REST API token. The server starts without it; until it is set, every Miro tool call returns an error that names the option and the configure command.

Get a token from https://miro.com/app/settings/user-profile/apps. Claude Code does not prompt for it at install, and an unset token does not put the plugin in the /plugin Errors view. Enter it with /plugin configure miro@melodic-software (masked input); until then each tool call fails with that instruction. Setting MIRO_API_TOKEN in the server's environment also works, and an empty value or the unexpanded ${user_config.miro_api_token} text counts as unset. Sensitive values use the macOS Keychain, or ~/.claude/.credentials.json on platforms where no supported keychain is available; the token is substituted into the server's MIRO_API_TOKEN environment variable at launch.

Run /miro:setup to check enablement and MCP availability or, with explicit confirmation, perform a minimal read-only API credential check. The setup skill never reads or exposes the token and never invokes a mutating Miro tool.

Rotating or clearing the token

Once set, a sensitive userConfig value has no dedicated reconfigure entry in the /plugin detail view, and the /mcp server menu's "Clear authentication" applies to OAuth-based servers only. It is not the rotation path for a token supplied through userConfig (the bundled stdio server receives miro_api_token as its MIRO_API_TOKEN environment variable, never through an OAuth flow). To change or clear the token at any time, run:

/plugin configure miro@<marketplace>

That reopens the same configuration screen shown at first enable, letting you overwrite or blank the stored token. Prefer it regardless. It masks input, where a token passed on the command line lands in shell history and the process table.

The older claim here, that --config is ignored once the plugin is installed, was never version-stamped. Headless --config against an already-installed plugin writes a non-sensitive option; whether that holds for a sensitive option such as miro_api_token has not been verified, so do not rely on it for a credential. Do not uninstall to rotate: that drops this plugin's entire stored pluginConfigs entry, resetting every option in the Options reference table below to its manifest default. The verified-version record lives in the plugin-reconfiguration convention.

Using vault-exec (opt-in)

If your machine resolves vendor API keys from a secret store at launch time instead of typing them into app UIs (see melodic-software/dotfiles ADR 0005), you can keep miro_api_token out of Claude Code's secure credential storage and let vault-exec hand the token to the server at launch instead. This is entirely opt-in. Skip it and the plugin behaves exactly as described above.

Leave miro_api_token unset. The plugin's own server then starts without a token and answers every tool call with the configure instruction, which is why the recipe below disables it per project and launches a different server process instead.

  1. Enable the plugin.

  2. Locate this plugin's cached server bundle. Claude Code copies an installed plugin into a version-keyed cache directory, ~/.claude/plugins/cache/<marketplace>/miro/<resolved-version>/server/dist/index.min.js. The exact path changes on every miro update, so re-check it after one.

  3. Add a user-scope stdio MCP server whose command wraps that path with vault-exec:

    claude mcp add --transport stdio --scope user miro \
      -- vault-exec --env MIRO_API_TOKEN=<your-secret-name> \
      -- node /path/to/cache/miro/<resolved-version>/server/dist/index.min.js

    The vault-exec.ps1 PowerShell wrapper resolves the same secret on Windows, but the shell a wrapped stdio command like this launches through on Windows is unverified. Confirm it starts before relying on it.

  4. Because that command differs from the plugin's own node ${CLAUDE_PLUGIN_ROOT}/... entry, Claude Code's endpoint-based deduplication does not collapse the two servers the way it does for an HTTP server at an identical URL (see the dometrain plugin's README for that case). Disable the plugin's own miro server with /mcp so only the wrapped one runs. /mcp's disabled-server choice is stored per project, so repeat this once in every project where you use miro.

Tool names move from mcp__plugin_miro_miro__<tool> (plugin-provided) to mcp__miro__<tool> (directly configured) once the override is active. Update any permission rule written against the old prefix. Basis: MCP server configuration, "Plugin MCP tool names" and "Server deduplication," verified 2026-09-23.

event-storming 0.6.14 and later detect Miro under either prefix, so its live-board path keeps working with this override. Earlier versions probe only mcp__plugin_miro_miro__* and fall back to markdown while the override is active.

Tools

The server registers Miro operations grouped by concern: boards, sticky notes, frames, tags, connectors, bulk create, and overlap detection. Read-only tools annotate readOnlyHint so Claude can parallelize them; mutating tools serialize.

Architecture

Stdio MCP server (@modelcontextprotocol/sdk) on Node ≥ 24. Cross-platform, no per-OS path divergence at the stdio boundary. Tool definitions are thin wrappers over the @mirohq/miro-api client; the request/response and error-shaping logic lives in server/src/.

The TypeScript in server/src/ is the single source of truth. server/dist/index.min.js is generated build output: an esbuild single-file bundle of the source and all runtime dependencies. Plugin install runs no build step, so the bundle is committed; CI rebuilds it from source with the pinned toolchain and fails on any drift, so the committed artifact is always exactly what the source produces.

The whole Node project (package.json, the lockfile, src/, dist/, and the tool configs) lives under server/ rather than at the plugin root. Claude Code runs npm ci --ignore-scripts inside a consumer's plugin cache whenever the plugin root holds both a package.json and a supported lockfile, and that install cannot be turned off; it would materialize this project's devDependencies (the TypeScript, biome, esbuild and vitest toolchain) on every install even though the bundle needs none of them at runtime. Keeping the project one level down leaves the plugin root without a lockfile, so nothing is installed, while CI and Dependabot still pin and rebuild from the same lockfile. Basis: plugins-reference.md, "Node.js package dependencies", verified 2026-09-11; recheck when that section changes.

Development

cd plugins/miro/server
npm install
npm run typecheck     # tsc --noEmit
npm test              # vitest (with coverage + typecheck)
npm run lint          # biome check
npm run bundle        # regenerate dist/index.min.js from src/
npm run verify-bundle # fail if dist/index.min.js drifts from src/

After editing server/src/, run npm run bundle and commit the regenerated server/dist/index.min.js alongside the source change.

Configuration

Options reference

Generated from this plugin's .claude-plugin/plugin.json. Every option Claude Code will prompt for when the plugin is enabled, with the environment variable each hook reads it from.

Option Type Default Environment variable Description
miro_api_token string (none) CLAUDE_PLUGIN_OPTION_MIRO_API_TOKEN Sensitive: stored in the OS keychain or protected credentials file. Miro REST API token from https://miro.com/app/settings/user-profile/apps. Until it is set, every Miro tool call returns an error that names this option and the configure command. Stored by Claude Code in secure credential storage, never settings.json.

How to set these

Three supported routes, in the order most people want them:

  1. Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later: /plugin configure miro@<marketplace>.

  2. Headless. Repeat --config for each option. Replace <marketplace> with the marketplace you installed this plugin from:

    claude plugin install miro@<marketplace> -s <scope> --config miro_api_token=<value>

    Route 1 is the rotation path for this plugin, not this one. Every option here is sensitive, and /plugin configure masks input. A secret passed on the command line lands in shell history and the process table. Do not rely on this command to rotate a credential; the verified-version record lives in the plugin-reconfiguration convention. Do not claude plugin uninstall to reconfigure either: uninstalling drops this plugin's whole stored pluginConfigs entry, resetting every option in the table above to its default.

  3. By hand, in settings. Add the value under pluginConfigs in your user settings (~/.claude/settings.json):

    {
      "pluginConfigs": {
        "miro@<marketplace>": {
          "options": {
            "miro_api_token": <value>
          }
        }
      }
    }

    Plugin option values are read from user, --settings, and managed settings only, not from a project's .claude/settings.json. To vary behavior per repository, enable or disable the plugin in that project's enabledPlugins instead of setting an option there.

Do not set the CLAUDE_PLUGIN_OPTION_* variables yourself. They are how Claude Code hands a configured value to a hook process; the value comes from the routes above.

Upstream documentation