Skip to main content
Glama

create_diagram_with_custom_icons

Create infrastructure diagrams with custom icons from URLs or files to represent services not in standard libraries, such as brand logos or GitHub avatars.

Instructions

Create diagrams with custom icons from web URLs or local files.

USE WHEN: Brand logos (Stripe, Vercel, Supabase, Fly.io) not in diagrams library. GitHub avatars work well: https://avatars.githubusercontent.com/u/{org_id}

Examples: URL: custom_nodes=[{"id":"stripe","icon_source":"url","icon_path":"https://avatars.githubusercontent.com/u/856813"}] Mixed: nodes=[{...AWS nodes...}], custom_nodes=[{...}], connections=[...]

HTTPS-only for URLs, 5MB limit, PNG/JPG supported. Automatic caching.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nameYesDiagram title
custom_nodesYesCustom nodes with icon URLs/paths
connectionsYesConnections between nodes
nodesNoOptional standard provider nodes to mix with custom
clustersNoOptional clusters
directionNoLayout directionLR
curvestyleNoEdge styleortho
output_formatNoOutput format(s): png, pdf, jpg, dotpng
output_dirNoOutput directory (default: current directory). Auto-created if missing.
graph_attrNoGraph attributes
return_base64NoReturn base64 images

Implementation Reference

  • The @mcp.tool decorator that registers the 'create_diagram_with_custom_icons' tool, including name, description, and annotations.
    @mcp.tool(
        name="create_diagram_with_custom_icons",
        description="""Create diagrams with custom icons from web URLs or local files.
    
    USE WHEN: Brand logos (Stripe, Vercel, Supabase, Fly.io) not in diagrams library.
    GitHub avatars work well: https://avatars.githubusercontent.com/u/{org_id}
    
    Examples:
    URL: custom_nodes=[{"id":"stripe","icon_source":"url","icon_path":"https://avatars.githubusercontent.com/u/856813"}]
    Mixed: nodes=[{...AWS nodes...}], custom_nodes=[{...}], connections=[...]
    
    HTTPS-only for URLs, 5MB limit, PNG/JPG supported. Automatic caching.""",
        annotations={
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": True,
        },
    )
  • Pydantic BaseModel CustomNodeDef defining the structure and validation for custom nodes used in the tool.
    class CustomNodeDef(BaseModel):
        """Definition of a custom node with icon from URL or local file."""
    
        id: str = Field(
            description="Unique node ID",
            min_length=1,
            max_length=200,
        )
        label: str = Field(
            description="Display label",
            min_length=1,
        )
        icon_source: Literal["url", "local"] = Field(description="Icon from web URL or local file")
        icon_path: str = Field(
            description="HTTPS URL or local file path",
            min_length=1,
        )
        cache_icons: bool = Field(
            default=True,
            description="Cache downloaded icons",
        )
    
        @field_validator("id")
        @classmethod
        def validate_id_format(cls, v: str) -> str:
            """Validate ID format."""
            if not re.match(r"^[a-zA-Z0-9_-]+$", v):
                raise ValueError(
                    f"Invalid node ID '{v}': only alphanumeric characters, "
                    "underscores, and hyphens allowed"
                )
            return v
    
        @field_validator("icon_path")
        @classmethod
        def validate_icon_path(cls, v: str, info) -> str:
            """Validate icon path format based on source type."""
            icon_source = info.data.get("icon_source")
    
            if icon_source == "url":
                if not v.startswith("https://"):
                    raise ValueError(f"Icon URL must start with 'https://', got: {v}")
            # For local paths, we'll validate existence at runtime
    
            return v
  • The core handler function that executes the tool: loads custom icons, builds diagram using diagrams.Diagram and Custom, handles mixed standard/custom nodes, connections, outputs in multiple formats.
    async def create_diagram_with_custom_icons(
        name: Annotated[str, Field(description="Diagram title")],
        custom_nodes: Annotated[
            List[CustomNodeDef], Field(description="Custom nodes with icon URLs/paths")
        ],
        connections: Annotated[List[ConnectionDef], Field(description="Connections between nodes")],
        nodes: Annotated[
            Optional[List[NodeDef]],
            Field(description="Optional standard provider nodes to mix with custom"),
        ] = None,
        clusters: Annotated[Optional[List[ClusterDef]], Field(description="Optional clusters")] = None,
        direction: Annotated[
            Literal["LR", "RL", "TB", "BT"], Field(description="Layout direction")
        ] = "LR",
        curvestyle: Annotated[Literal["ortho", "curved"], Field(description="Edge style")] = "ortho",
        output_format: Annotated[
            str | List[str], Field(description="Output format(s): png, pdf, jpg, dot")
        ] = "png",
        output_dir: Annotated[
            Optional[str],
            Field(
                description="Output directory (default: current directory). Auto-created if missing."
            ),
        ] = None,
        graph_attr: Annotated[Optional[Dict[str, Any]], Field(description="Graph attributes")] = None,
        return_base64: Annotated[bool, Field(description="Return base64 images")] = False,
    ) -> str:
        """Generate diagram with custom node icons."""
        start_time = time.time()
    
        try:
            from diagrams.custom import Custom
    
            # Initialize icon manager
            icon_manager = IconManager()
    
            # Validate all custom icons and get paths
            custom_icon_paths = {}
            for custom_node in custom_nodes:
                try:
                    icon_path = icon_manager.get_icon_path(
                        custom_node.icon_source,
                        custom_node.icon_path,
                        cache=custom_node.cache_icons,
                    )
                    custom_icon_paths[custom_node.id] = icon_path
                except Exception as e:
                    return format_error(
                        f"Failed to load icon for node '{custom_node.id}': {str(e)}",
                        suggestion="Check icon_path is valid HTTPS URL or existing local file",
                    )
    
            # Build combined node ID set
            node_ids = {node.id for node in custom_nodes}
            if nodes:
                node_ids.update({node.id for node in nodes})
    
            # Validate connections
            for conn in connections:
                if conn.from_node not in node_ids:
                    raise ValueError(f"Connection references unknown node '{conn.from_node}'")
    
                targets = [conn.to_node] if isinstance(conn.to_node, str) else conn.to_node
                for target in targets:
                    if target not in node_ids:
                        raise ValueError(f"Connection references unknown node '{target}'")
    
            # Prepare output
            formats = [output_format] if isinstance(output_format, str) else output_format
    
            # Reject SVG - it's buggy and unsupported
            if any("svg" in fmt.lower() for fmt in formats):
                raise ValueError("SVG output is not supported. Use png, pdf, jpg, or dot instead.")
    
            # Change directory if needed
            original_dir = os.getcwd()
            if output_dir:
                os.makedirs(output_dir, exist_ok=True)
                os.chdir(output_dir)
    
            try:
                # Hide diagram title by setting label to empty string
                default_graph_attr = {"label": ""}
                merged_graph_attr = {**default_graph_attr, **(graph_attr or {})}
    
                with Diagram(
                    name=name,
                    show=False,
                    direction=direction,
                    curvestyle=curvestyle,
                    outformat=formats,
                    graph_attr=merged_graph_attr,
                ) as _:
                    # Create all nodes
                    node_objects = {}
    
                    # Create custom nodes
                    for custom_node in custom_nodes:
                        icon_path = custom_icon_paths[custom_node.id]
                        node_obj = Custom(custom_node.label, icon_path)
                        node_objects[custom_node.id] = node_obj
    
                    # Create standard nodes if provided
                    if nodes:
                        for node in nodes:
                            NodeClass = import_node_class(node.provider, node.category, node.type)
                            node_obj = NodeClass(node.label)
                            node_objects[node.id] = node_obj
    
                    # Create connections
                    edge_count = 0
                    for conn in connections:
                        from_obj = node_objects[conn.from_node]
                        targets = [conn.to_node] if isinstance(conn.to_node, str) else conn.to_node
    
                        for target in targets:
                            to_obj = node_objects[target]
    
                            if conn.label or conn.color or conn.style:
                                edge = Edge(
                                    label=conn.label or "",
                                    color=conn.color or "black",
                                    style=conn.style or "solid",
                                )
                                if conn.direction == "forward":
                                    _ = from_obj >> edge >> to_obj
                                elif conn.direction == "reverse":
                                    _ = from_obj << edge << to_obj
                                else:
                                    _ = from_obj - edge - to_obj
                            else:
                                if conn.direction == "forward":
                                    _ = from_obj >> to_obj
                                elif conn.direction == "reverse":
                                    _ = from_obj << to_obj
                                else:
                                    _ = from_obj - to_obj
    
                            edge_count += 1
    
                # Get file paths
                diagram_filename = name.replace(" ", "_").replace("-", "_").lower()
                file_paths = []
                for fmt in formats:
                    file_path = f"{diagram_filename}.{fmt}"
                    if output_dir:
                        file_path = os.path.join(output_dir, file_path)
                    file_paths.append(os.path.abspath(file_path))
    
                # Build metadata
                generation_time_ms = (time.time() - start_time) * 1000
                total_nodes = len(custom_nodes) + (len(nodes) if nodes else 0)
                metadata = build_diagram_metadata(
                    file_paths,
                    node_count=total_nodes,
                    edge_count=edge_count,
                    cluster_count=len(clusters) if clusters else 0,
                    generation_time_ms=generation_time_ms,
                )
    
                # Base64 encoding if requested
                base64_images = None
                if return_base64:
                    base64_images = {}
                    for path in file_paths:
                        ext = Path(path).suffix[1:]
                        if ext != "dot":
                            try:
                                base64_images[ext] = encode_file_base64(path)
                            except Exception:
                                pass
    
                return format_diagram_result(file_paths, metadata, base64_images)
    
            finally:
                if output_dir:
                    os.chdir(original_dir)
    
        except Exception as e:
            return format_error(f"Failed to generate diagram with custom icons: {str(e)}")
  • Key helper method in IconManager class used by the handler to resolve and validate custom icon paths from URL (with download/cache) or local files.
    def get_icon_path(
        self,
        icon_source: str,
        icon_path: str,
        cache: bool = True,
    ) -> str:
        """Get icon path, downloading from URL or validating local file.
    
        This is the main entry point for getting icon paths.
    
        Args:
            icon_source: Either "url" or "local"
            icon_path: URL or local file path
            cache: Whether to cache downloaded icons
    
        Returns:
            Absolute path to the icon file
    
        Raises:
            ValueError: If icon source is invalid or icon cannot be retrieved
        """
        if icon_source == "url":
            return self.download_icon(icon_path, cache=cache)
        elif icon_source == "local":
            return self.validate_local_icon(icon_path)
        else:
            raise ValueError(f"Invalid icon_source '{icon_source}'. Must be 'url' or 'local'")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/apetta/diagrams-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server