Skip to main content
Glama

Mural MCP

MCP server for working with Mural Public API v1 from MCP-compatible clients (Cursor, Claude Desktop, and other MCP hosts).
It supports browsing workspaces and rooms, creating/updating/deleting murals, and creating/updating/deleting mural widgets.

Description

This project exposes Mural operations as MCP tools by wrapping Mural's REST API with a Python server built on FastMCP.

Highlights:

  • OAuth2 authentication using client_id, client_secret, and a long-lived refresh_token

  • Automatic access token refresh before expiration

  • Tooling for:

    • workspace/room/mural navigation

    • mural CRUD

    • widget CRUD (sticky notes, shapes, text, arrows, areas, comments)

Related MCP server: miro-mcp-server

Requirements

  • Python >=3.11

  • uv (recommended for dependency management and running)

  • A Mural developer app with:

    • MURAL_CLIENT_ID

    • MURAL_CLIENT_SECRET

    • OAuth redirect URI configured in Mural

Installation

Clone the repository and install dependencies:

git clone <your-repo-url>
cd arcsen-mural-mcp
uv sync

Configuration

Create a .env file in the project root:

cp .env.example .env

Fill in:

MURAL_CLIENT_ID=your-client-id
MURAL_CLIENT_SECRET=your-client-secret
MURAL_REFRESH_TOKEN=your-refresh-token
# Optional but recommended:
MURAL_REDIRECT_URI=http://localhost:8080/callback

Get Initial Refresh Token (One-Time OAuth Setup)

If you only have client ID and secret, generate the first refresh token with:

./scripts/get_mural_refresh_token.sh

What it does:

  1. Reads MURAL_CLIENT_ID and MURAL_CLIENT_SECRET from .env

  2. Builds OAuth authorization URL

  3. Prompts you for the returned authorization code

  4. Exchanges code for tokens and prints MURAL_REFRESH_TOKEN=...

Then paste that value into your .env.

Note: redirect URI must match exactly between:

  • your Mural app settings

  • the authorization request

  • the token exchange request

Run the MCP Server

Start server over stdio:

uv run main.py

This process is intended to be launched by an MCP host (rather than visited in a browser).

Connect to Claude Desktop

Edit:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Example:

{
  "mcpServers": {
    "mural": {
      "command": "uv",
      "args": [
        "run",
        "--directory",
        "/absolute/path/to/arcsen-mural-mcp",
        "main.py"
      ],
      "env": {
        "MURAL_CLIENT_ID": "your-client-id",
        "MURAL_CLIENT_SECRET": "your-client-secret",
        "MURAL_REFRESH_TOKEN": "your-refresh-token"
      }
    }
  }
}

Restart Claude Desktop after saving.

Available Tool Groups

The server includes the following tool categories:

  • Navigation / Read

    • list_workspaces

    • list_rooms

    • list_murals_in_room

    • get_mural

    • get_mural_widgets

    • get_mural_widget

  • Mural CRUD

    • create_mural

    • update_mural

    • delete_mural

  • Widget Create

    • create_sticky_notes

    • create_shapes

    • create_titles

    • create_textboxes

    • create_arrow

    • create_area

    • create_comment

  • Widget Update

    • update_sticky_note

    • update_shape

    • update_text

    • update_arrow

    • update_area

    • update_comment

  • Widget Delete

    • delete_widget

Arrow Accuracy Tip

To avoid randomly placed arrows, prefer anchored connectors:

  • Use create_arrow with start_ref_id and end_ref_id

  • Use widget IDs from created/fetched source and target shapes

  • Avoid relying only on raw x/y and points

Troubleshooting

  • RuntimeError for missing env vars
    Ensure MURAL_CLIENT_ID, MURAL_CLIENT_SECRET, and MURAL_REFRESH_TOKEN are present.

  • OAuth redirect shows "site can't be reached"
    This can still be fine if URL contains ?code=.... Copy the code from the URL and continue.

  • OAuth redirect returns 404 on local server
    Usually means redirect reached your local server but no matching path exists. You can still copy code from the URL.

  • No tools appear in Claude Desktop
    Validate JSON config, confirm absolute path, and restart Claude Desktop fully.

Development Notes

  • Entry point: main.py

  • Auth and token refresh logic: MuralClient class in main.py

  • OAuth helper script: scripts/get_mural_refresh_token.sh

License

Add your project license here (for example, MIT).

Available Tools

23 tools
create_areaA

Create an area (swimlane/zone) widget on a mural.

Args:
    mural_id: The mural ID.
    x: Horizontal position in px (default 0).
    y: Vertical position in px (default 0).
    width: Width in px (default 460).
    height: Height in px (default 503).
    title: Title text displayed on the area.
    layout: "free" (default), "column", or "row".
    show_title: Whether to display the title (default true).
    style: { backgroundColor, borderColor, borderStyle ("solid"|"dotted"), borderWidth (1-7), titleFontSize }.
    parent_id: ID of parent area widget (for nested areas).

Inputs: mural_id (required), x, y, style, +6 more. Returns: result

create_arrowA

Create an arrow (connector) widget on a mural.

Args:
    mural_id: The mural ID.
    x: Horizontal position in px.
    y: Vertical position in px.
    width: Width of the bounding box in px.
    height: Height of the bounding box in px.
    points: Array of {x, y} coordinate objects defining the arrow path (min 2 points).
    arrow_type: "straight" (default), "curved", or "orthogonal".
    tip: "single" (default), "double", or "no tip".
    start_ref_id: ID of the widget the arrow starts from (snaps to it).
    end_ref_id: ID of the widget the arrow ends at (snaps to it).
    style: { strokeColor (hex+alpha), strokeStyle ("solid"|"dashed"|"dotted-spaced"|"dotted"), strokeWidth (1-7) }.
    label: { format: {color, fontFamily, bold, italic, textAlign, fontSize}, labels: [{x, y, height, width, text}] }.
    parent_id: ID of parent area widget.

Inputs: x (required), y (required), width (required), height (required), +9 more. Returns: result

create_commentA

Create a comment widget on a mural.

Args:
    mural_id: The mural ID.
    x: Horizontal position in px.
    y: Vertical position in px.
    message: The comment text.
    reference_widget_id: ID of the widget this comment is attached to.
    parent_id: ID of parent area widget.

Inputs: x (required), y (required), message (required), mural_id (required), +2 more. Returns: result

create_muralA

Create a new mural in a room.

Args:
    room_id: The room ID to create the mural in (required).
    title: Title of the mural.
    background_color: Background color in hex with alpha (e.g. "#FFFFFFFF").
    width: Width of the mural in px (3000-60000, default 9216).
    height: Height of the mural in px (3000-60000, default 6237).
    infinite: If true, canvas is borderless and grows as you add widgets.

Inputs: room_id (required), title, width, height, +2 more. Returns: result

create_shapesA

Create one or more shape widgets on a mural (limit 1000).

Each widget object supports:
  - x, y (required): Position in px.
  - shape (required): One of: ellipse, rectangle, rounded_square, rhombus_smart, triangle_smart,
    hexagon_smart, pentagon_smart, diamond, process, decision, terminator, start, end, delay,
    data, database, document, multiple_documents, predefined_process, manual_input, manual_loop,
    preparation, stored_data, connector, merge, or, summing_junction, display, direct_data,
    internal_storage, papertape, loop_limit, off_page_connector, cloud, cross, star, badge,
    ribbon, step, trapezoid, octagon, speech_bubble_left, speech_bubble_right, speech_bubble_center,
    thinking_bubble_left, thinking_bubble_right, arrow_right, arrow_left, arrow_down, arrow_top,
    arrow_left_right, brace_left, brace_right, note_left, note_right, teardrop_bubble, right_triangle.
  - text: Plain text content.
  - htmlText: HTML-formatted text.
  - width, height: Size in px (default 138).
  - style: { backgroundColor, borderColor, borderStyle, borderWidth, bold, italic, underline, strike, font, fontColor, fontSize, textAlign }.
  - parentId: ID of parent area widget.

Args:
    mural_id: The mural ID.
    widgets: Array of shape objects.

Inputs: widgets (required), mural_id (required). Returns: result

create_sticky_notesA

Create one or more sticky note widgets on a mural (limit 1000).

Each widget object supports:
  - x, y (required): Position in px.
  - shape (required): "circle" or "rectangle".
  - text: Plain text content.
  - htmlText: HTML-formatted text (overrides text).
  - width, height: Size in px (default 138).
  - style: { backgroundColor, bold, italic, underline, strike, font, fontSize, textAlign, border }.
  - parentId: ID of parent area widget.
  - tags: Array of tag IDs.

Args:
    mural_id: The mural ID.
    widgets: Array of sticky note objects.

Inputs: widgets (required), mural_id (required). Returns: result

create_textboxesA

Create one or more textbox widgets on a mural (limit 1000). Textboxes wrap text to fixed width.

Each widget object supports:
  - text: The textbox text (supports HTML: <b>, <i>, <u>, <s>, <span style="color:...">>).
  - x, y: Position in px (default 0).
  - width: Width in px (default 296).
  - height: Height in px (default 28).
  - style: { backgroundColor, font, fontSize (default 24), textAlign }.
  - parentId: ID of parent area widget.

Args:
    mural_id: The mural ID.
    widgets: Array of textbox objects.

Inputs: widgets (required), mural_id (required). Returns: result

create_titlesA

Create one or more title widgets on a mural (limit 1000). Titles auto-grow width to fit text.

Each widget object supports:
  - text: The title text (supports HTML: <b>, <i>, <u>, <s>, <span style="color:...">>).
  - x, y: Position in px (default 0).
  - width: Width in px (default 293).
  - height: Height in px (default 62).
  - style: { backgroundColor, font, fontSize (default 48), textAlign }.
  - parentId: ID of parent area widget.

Args:
    mural_id: The mural ID.
    widgets: Array of title objects.

Inputs: widgets (required), mural_id (required). Returns: result

delete_muralB

Delete a mural.

Args:
    mural_id: The mural ID to delete.

Inputs: mural_id (required). Returns: result

delete_widgetB

Delete any widget from a mural by its ID.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID to delete.

Inputs: mural_id (required), widget_id (required). Returns: result

get_muralA

Get details of a single mural.

Args:
    mural_id: The mural ID (format: "workspaceId.timestamp", e.g. "workspace1234.1608152669000").

Inputs: mural_id (required). Returns: result

get_mural_widgetC

Get a single widget by its ID.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID.

Inputs: mural_id (required), widget_id (required). Returns: result

get_mural_widgetsA

Get all widgets on a mural. Use type_filter to narrow results.

Args:
    mural_id: The mural ID.
    type_filter: Comma-separated widget types to filter. Options: areas, arrows, comments, files, sticky notes, texts, icons, images, shapes.
    parent_id: Filter widgets by their parent area ID.
    limit: Max results to return.
    next_token: Pagination token from a previous response.

Inputs: mural_id (required), limit, parent_id, next_token, +1 more. Returns: result

list_murals_in_roomA

List murals in a room.

Args:
    room_id: The room ID (integer).
    limit: Max results to return.
    next_token: Pagination token from a previous response.

Inputs: room_id (required), limit, next_token. Returns: result

list_roomsA

List rooms in a workspace that the authenticated user has access to.

Args:
    workspace_id: The workspace ID.
    limit: Max results to return.
    next_token: Pagination token from a previous response.

Inputs: workspace_id (required), limit, next_token. Returns: result

list_workspacesA

List all workspaces the authenticated user is a member of.

Args:
    limit: Max results to return.
    next_token: Pagination token from a previous response.

Inputs: limit, next_token. Returns: result

update_areaA

Update an area (swimlane/zone) widget on a mural.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID of the area.
    properties: Object with fields to update. Supported fields:
        x, y, width, height, title, layout ("free"|"column"|"row"), showTitle,
        style: { backgroundColor, borderColor, borderStyle, borderWidth, titleFontSize },
        parentId, hidden.

Inputs: mural_id (required), widget_id (required), properties (required). Returns: result

update_arrowA

Update an arrow (connector) widget on a mural.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID of the arrow.
    properties: Object with fields to update. Supported fields:
        x, y, width, height, points, arrowType ("straight"|"curved"|"orthogonal"),
        tip ("single"|"double"|"no tip"), startRefId, endRefId,
        style: { strokeColor, strokeStyle, strokeWidth },
        label: { format: {...}, labels: [...] }, parentId.

Inputs: mural_id (required), widget_id (required), properties (required). Returns: result

update_commentA

Update a comment widget on a mural.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID of the comment.
    properties: Object with fields to update. Supported fields:
        x, y, message, referenceWidgetId, parentId.

Inputs: mural_id (required), widget_id (required), properties (required). Returns: result

update_muralC

Update a mural's properties.

Args:
    mural_id: The mural ID.
    title: New title.
    background_color: New background color in hex with alpha.

Inputs: mural_id (required), title, background_color. Returns: result

update_shapeA

Update a shape widget on a mural.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID of the shape.
    properties: Object with fields to update. Supported fields:
        x, y, width, height, rotation, text, htmlText,
        style: { backgroundColor, borderColor, borderStyle, borderWidth, bold, italic, underline, strike, font, fontColor, fontSize, textAlign },
        title, parentId, hidden.

Inputs: mural_id (required), widget_id (required), properties (required). Returns: result

update_sticky_noteB

Update a sticky note widget on a mural.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID of the sticky note.
    properties: Object with fields to update. Supported fields:
        x, y, width, height, rotation, text, htmlText,
        style: { backgroundColor, bold, italic, underline, strike, font, fontSize, textAlign, border },
        tags, title, hyperlink, hyperlinkTitle, parentId, hidden.

Inputs: mural_id (required), widget_id (required), properties (required). Returns: result

update_textA

Update a text widget (title or textbox) on a mural.

Args:
    mural_id: The mural ID.
    widget_id: The widget ID of the text/title/textbox.
    properties: Object with fields to update. Supported fields:
        x, y, width, height, rotation, text,
        style: { backgroundColor, font, fontSize, textAlign },
        title, hyperlink, hyperlinkTitle, parentId, hidden.

Inputs: mural_id (required), widget_id (required), properties (required). Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 23 tools

    Disambiguation5/5

    Each tool targets a distinct resource and action: workspace, room, mural, and specific widget types (sticky note, shape, title, textbox, arrow, area, comment). The list/get/create/update/delete operations are clearly separated, and even similarly named tools like get_mural_widgets vs get_mural_widget are unambiguous.

    Naming Consistency4/5

    The overall pattern is consistent verb_noun (list_* get_* create_* update_* delete_*). Minor deviations include plural create_sticky_notes/create_shapes vs singular create_arrow/create_area, and generic delete_widget vs delete_mural, but these are readable and predictable.

    Tool Count3/5

    With 23 tools, the set is on the heavy side but appropriate for the domain, covering mural CRUD plus multiple widget types. It feels slightly bloated because widget creation/update could potentially be consolidated, but each tool serves a distinct purpose.

    Completeness4/5

    The core lifecycle for murals and widgets is well covered: list/get/create/update/delete for murals, and get/create/update/delete for widgets. Missing workspace/room creation and a few niche widget operations are minor gaps that agents can work around.

    Maintenance

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers