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