Skip to main content
Glama

create_diagram_with_custom_icons

Idempotent

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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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'")
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds valuable behavioral context beyond annotations: HTTPS-only requirement, 5MB file limit, PNG/JPG support, and automatic caching. Annotations cover idempotency and non-destructive nature, but the description supplements with operational constraints, though it doesn't mention rate limits or authentication needs, keeping it from a perfect score.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections: purpose, usage guidelines, examples, and constraints. Each sentence adds value, such as the HTTPS-only note and caching info, with no redundant or verbose content. It's front-loaded with key information and efficiently organized.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (11 parameters, nested objects in schema) and the presence of an output schema, the description is complete. It covers purpose, usage, examples, and constraints, while annotations handle safety aspects. The output schema likely explains return values, so the description doesn't need to, making it sufficiently comprehensive for agent use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema description coverage, the baseline is 3. The description provides examples for 'custom_nodes' and mentions mixing with standard nodes, adding some practical meaning. However, it doesn't detail other parameters like 'output_format' or 'graph_attr' beyond what the schema already explains, so it meets but doesn't exceed the baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool creates diagrams with custom icons from web URLs or local files, which is a specific verb+resource combination. It distinguishes from sibling tools like 'create_diagram' by emphasizing the custom icon capability, making the purpose unambiguous and differentiated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes an explicit 'USE WHEN' section that provides clear guidance: use for brand logos not in the diagrams library, with GitHub avatars as a good example. It distinguishes this tool from alternatives by specifying when custom icons are needed versus standard diagrams, offering practical context for selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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