├── .gitignore ├── README.md ├── __init__.py ├── generator.py ├── images ├── arcade_enterprise_banking.png ├── blueprint-mcp.png └── langgraph_architecture_learning_card.png ├── prompts.py ├── pyproject.toml └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blueprint MCP 2 | 3 | ![Blueprint MCP](images/blueprint-mcp.png) 4 | 5 | *Image generated using Blueprint MCP, Nano Banana Pro, and Arcade MCP server.* 6 | 7 | Diagram generation for understanding codebases and system architecture using Nano Banana Pro. 8 | 9 | **Works with Arcade's ecosystem:** Combine with HubSpot, Google Drive, GitHub, and other Arcade tools to extract data from your systems and visualize it as diagrams. 10 | 11 | ## Setup 12 | 13 | ### 1. Sign up for Arcade 14 | https://arcade.dev 15 | 16 | ### 2. Install Dependencies 17 | ```bash 18 | # Create virtual environment 19 | python3 -m venv venv 20 | source venv/bin/activate # On Windows: venv\Scripts\activate 21 | 22 | # Install Arcade CLI 23 | pip install arcade-mcp 24 | ``` 25 | 26 | ### 3. Login to Arcade 27 | ```bash 28 | arcade-mcp login 29 | ``` 30 | 31 | ### 4. Get Google AI Studio API Key 32 | https://aistudio.google.com/ → Create API key 33 | 34 | ### 5. Store Secret in Arcade 35 | ```bash 36 | arcade-mcp secret set GOOGLE_API_KEY="your_api_key_here" 37 | ``` 38 | 39 | ### 6. Deploy Server 40 | ```bash 41 | cd architect_mcp 42 | arcade-mcp deploy 43 | ``` 44 | 45 | ### 7. Create Gateway 46 | 1. Go to https://api.arcade.dev/dashboard 47 | 2. Click "Gateways" → "Create Gateway" 48 | 3. Add your deployed `architect_mcp` server to the gateway 49 | 50 | ### 8. Configure Cursor 51 | 1. In Cursor: Settings → MCP 52 | 2. Add your Arcade gateway URL 53 | 3. Restart Cursor 54 | 55 | ## Usage 56 | 57 | ### Tools 58 | 59 | - `start_diagram_job` - Start generation, returns job ID 60 | - `check_job_status` - Check if complete 61 | - `download_diagram` - Download PNG as base64 62 | 63 | ### Example Prompts 64 | 65 | **Visualize code architecture:** 66 | ``` 67 | Analyze the authentication module in src/auth/ and create an 68 | architecture diagram showing the components and their relationships. 69 | ``` 70 | 71 | **Document API flows:** 72 | ``` 73 | Create a sequence diagram showing the OAuth login flow based on 74 | the code in src/auth/oauth.py 75 | ``` 76 | 77 | **Explain processes:** 78 | ``` 79 | Generate a flowchart explaining how our payment processing works, 80 | showing the steps from checkout to confirmation. 81 | ``` 82 | 83 | **Understand data pipelines:** 84 | ``` 85 | Create a data flow diagram for our ETL pipeline showing sources, 86 | transformations, and destinations based on the data/ directory. 87 | ``` 88 | 89 | **Combine with other Arcade tools:** 90 | ``` 91 | Pull the latest deal from HubSpot for "Acme Corp" and create an 92 | architecture diagram of the proposed solution. 93 | ``` 94 | 95 | ``` 96 | Read the system design doc from Google Drive and generate a 97 | visual architecture diagram from it. 98 | ``` 99 | 100 | ## How It Works 101 | 102 | 1. `start_diagram_job` → Returns job ID instantly 103 | 2. Wait 30 seconds (Nano Banana Pro generates) 104 | 3. `check_job_status` → Check if "Complete" 105 | 4. `download_diagram` → Get base64 PNG 106 | 5. Agent decodes and saves to workspace 107 | 108 | --- 109 | 110 | ## Example Diagrams 111 | 112 | ### Enterprise Architecture - Banking Use Case 113 | 114 | **Cursor Prompt:** 115 | ``` 116 | Can you understand Arcade deeply and create an architecture diagram for someone 117 | who's new and wants to understand Arcade in the broader AI, LLM, and agent landscape? 118 | I want this architecture to fit into a realistic enterprise scenario like a bank, 119 | showcasing how Arcade MCP Runtime fits into their broader architecture. 120 | 121 | https://docs.arcade.dev/llms.txt 122 | ``` 123 | 124 | *Prompt received by Blueprint MCP tool: "Create enterprise architecture diagram with 5 layers: LAYER 1 End Users (Customer Service Agents, Loan Officers, Compliance Team, IT Ops), LAYER 2 Banking AI Assistant (Cursor IDE / Custom UI), LAYER 3 AI Layer showing GPT-4/Claude and LangChain/CrewAI (Model-Agnostic), LAYER 4 Arcade MCP Runtime (large box) containing Runtime Components (MCP Gateway, Tool Registry, OAuth 2.0 Auth, Secret Management, Session Manager) AND Hosted MCP Servers section with 6 MCP servers (Salesforce, Email/Gmail, Slack, Database, Document, Custom Banking) ALL INSIDE the Arcade box, LAYER 5 Bank's Existing Infrastructure (Core Banking System, CRM Salesforce, Compliance Database, Document Repository, Communication Systems, Legacy APIs). Show data flow arrows with labels (Tool Calls, Authenticated Requests, API Calls). Use technical whiteboard style, muted colors (gray, light blue, purple, orange), monospace fonts, 16:9."* 125 | 126 | ![Enterprise Banking Architecture](images/arcade_enterprise_banking.png) 127 | 128 | ### LangGraph Architecture Learning Card 129 | 130 | **Cursor Prompt:** 131 | ``` 132 | Help me understand the LangGraph architecture better. I have checked out the 133 | LangGraph codebase here: 134 | 135 | /Users/guru/dev/nano/langgraph 136 | 137 | Can you do a thorough analysis and help me understand the architecture? I would 138 | love to know details in a visual image: The key components involved, the flows, 139 | and can you create like one fully visual learning card sort of thing that helps 140 | me understand the architecture which I can print and give it to my fellow 141 | architects and help them learn? 142 | ``` 143 | 144 | *Prompt received by Blueprint MCP tool: "Create LangGraph architecture learning card with 6 sections: Core Components (State, Nodes, Edges with flow diagram), StateGraph Class workflow (Define → Build → Compile → Execute), Pregel-Inspired Execution showing super-steps with parallel/sequential execution examples, Checkpointing System (BaseCheckpointSaver, checkpoint-postgres/sqlite, state snapshots flow), Monorepo Structure (langgraph core, prebuilt, checkpoint libs, cli, sdk-py, sdk-js), and Capabilities (Durable execution, Human-in-the-loop, Memory, Streaming, Multi-agent, Sub-graphs). Use technical whiteboard style, muted colors (blue, gray, purple, green, orange), monospace fonts for code terms, information-dense layout, 16:9, printable quality."* 145 | 146 | ![LangGraph Architecture](images/langgraph_architecture_learning_card.png) 147 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """Blueprint MCP""" 2 | __version__ = "3.0.0" 3 | -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from datetime import datetime 3 | from io import BytesIO 4 | from pathlib import Path 5 | from typing import Optional 6 | 7 | from PIL import Image 8 | from google import genai 9 | from google.genai import types 10 | from pydantic import BaseModel, Field 11 | 12 | from prompts import AspectRatio, ImageSize 13 | 14 | 15 | class GenerationConfig(BaseModel): 16 | aspect_ratio: AspectRatio = Field(default=AspectRatio.LANDSCAPE) 17 | image_size: ImageSize = Field(default=ImageSize.HIGH) 18 | temperature: float = Field(default=1.0, ge=0.0, le=2.0) 19 | top_p: float = Field(default=0.95, ge=0.0, le=1.0) 20 | 21 | 22 | class GenerationResult(BaseModel): 23 | success: bool 24 | file_path: Optional[str] = None 25 | width: Optional[int] = None 26 | height: Optional[int] = None 27 | error: Optional[str] = None 28 | model_used: str = "gemini-3-pro-image-preview" 29 | 30 | 31 | class NanoBananaPro: 32 | MODEL_NAME = "gemini-3-pro-image-preview" 33 | MAX_OUTPUT_TOKENS = 32768 34 | 35 | def __init__(self, api_key: str, output_dir: Optional[Path] = None): 36 | self.api_key = api_key 37 | self.output_dir = output_dir or Path.cwd() 38 | self.output_dir.mkdir(parents=True, exist_ok=True) 39 | self.client = genai.Client(api_key=api_key) 40 | 41 | def generate( 42 | self, 43 | prompt: str, 44 | config: GenerationConfig = GenerationConfig(), 45 | filename_prefix: str = "diagram" 46 | ) -> GenerationResult: 47 | try: 48 | contents = [ 49 | types.Content( 50 | role="user", 51 | parts=[types.Part(text=prompt)] 52 | ) 53 | ] 54 | 55 | generation_config = types.GenerateContentConfig( 56 | temperature=config.temperature, 57 | top_p=config.top_p, 58 | max_output_tokens=self.MAX_OUTPUT_TOKENS, 59 | response_modalities=["IMAGE"], 60 | safety_settings=[ 61 | types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="OFF"), 62 | types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="OFF"), 63 | types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="OFF"), 64 | types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="OFF") 65 | ], 66 | image_config=types.ImageConfig( 67 | aspect_ratio=config.aspect_ratio.value, 68 | image_size=config.image_size.value, 69 | ), 70 | ) 71 | 72 | response = self.client.models.generate_content( 73 | model=self.MODEL_NAME, 74 | contents=contents, 75 | config=generation_config, 76 | ) 77 | 78 | image_data = self._extract_image_data(response) 79 | if not image_data: 80 | return GenerationResult(success=False, error="No image data in response") 81 | 82 | file_path, width, height = self._save_image(image_data, filename_prefix) 83 | 84 | return GenerationResult( 85 | success=True, 86 | file_path=str(file_path), 87 | width=width, 88 | height=height 89 | ) 90 | 91 | except Exception as e: 92 | return GenerationResult(success=False, error=self._format_error(e)) 93 | 94 | def _extract_image_data(self, response) -> Optional[bytes]: 95 | if not response.candidates: 96 | return None 97 | 98 | for part in response.candidates[0].content.parts: 99 | if hasattr(part, 'inline_data') and part.inline_data: 100 | img_data = part.inline_data.data 101 | if isinstance(img_data, str): 102 | return base64.b64decode(img_data) 103 | else: 104 | return img_data 105 | 106 | return None 107 | 108 | def _save_image(self, image_data: bytes, prefix: str) -> tuple[Path, int, int]: 109 | image = Image.open(BytesIO(image_data)) 110 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 111 | filename = f"{prefix}_{timestamp}.png" 112 | file_path = self.output_dir / filename 113 | image.save(file_path, "PNG") 114 | return file_path, image.width, image.height 115 | 116 | def _format_error(self, error: Exception) -> str: 117 | error_str = str(error) 118 | 119 | if "quota" in error_str.lower(): 120 | return f"API quota exceeded: {error_str}" 121 | elif "401" in error_str or "authentication" in error_str.lower(): 122 | return f"Authentication failed: {error_str}" 123 | elif "404" in error_str or "not found" in error_str.lower(): 124 | return f"Model not found: {error_str}" 125 | elif "billing" in error_str.lower(): 126 | return f"Billing required: {error_str}" 127 | else: 128 | return f"Generation failed: {error_str}" 129 | 130 | 131 | class DiagramGenerator: 132 | def __init__(self, api_key: str, output_dir: Optional[Path] = None): 133 | self.client = NanoBananaPro(api_key=api_key, output_dir=output_dir) 134 | 135 | def generate_from_prompt( 136 | self, 137 | prompt: str, 138 | aspect_ratio: str = "16:9", 139 | resolution: str = "2K", 140 | filename_prefix: str = "diagram" 141 | ) -> GenerationResult: 142 | config = GenerationConfig( 143 | aspect_ratio=AspectRatio(aspect_ratio), 144 | image_size=ImageSize(resolution) 145 | ) 146 | 147 | return self.client.generate( 148 | prompt=prompt, 149 | config=config, 150 | filename_prefix=filename_prefix 151 | ) 152 | -------------------------------------------------------------------------------- /images/arcade_enterprise_banking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArcadeAI/blueprint-mcp/e843005cf3e7fd8f823499ead9d603b5e78a3801/images/arcade_enterprise_banking.png -------------------------------------------------------------------------------- /images/blueprint-mcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArcadeAI/blueprint-mcp/e843005cf3e7fd8f823499ead9d603b5e78a3801/images/blueprint-mcp.png -------------------------------------------------------------------------------- /images/langgraph_architecture_learning_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArcadeAI/blueprint-mcp/e843005cf3e7fd8f823499ead9d603b5e78a3801/images/langgraph_architecture_learning_card.png -------------------------------------------------------------------------------- /prompts.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class DiagramType(str, Enum): 5 | ARCHITECTURE = "architecture" 6 | FLOWCHART = "flowchart" 7 | DATA_FLOW = "data_flow" 8 | SEQUENCE = "sequence" 9 | INFOGRAPHIC = "infographic" 10 | GENERIC = "generic" 11 | 12 | 13 | class AspectRatio(str, Enum): 14 | SQUARE = "1:1" 15 | PORTRAIT = "9:16" 16 | LANDSCAPE = "16:9" 17 | WIDE = "21:9" 18 | VERTICAL_PORTRAIT = "3:4" 19 | HORIZONTAL_LANDSCAPE = "4:3" 20 | 21 | 22 | class ImageSize(str, Enum): 23 | STANDARD = "1K" 24 | HIGH = "2K" 25 | 26 | 27 | def optimize_prompt_for_nano_banana( 28 | base_prompt: str, 29 | diagram_type: DiagramType, 30 | aspect_ratio: AspectRatio = AspectRatio.LANDSCAPE, 31 | image_size: ImageSize = ImageSize.HIGH, 32 | emphasis_on_text: bool = True 33 | ) -> str: 34 | requirements = [] 35 | 36 | if emphasis_on_text: 37 | requirements.append( 38 | "All text must be crystal clear and perfectly legible. " 39 | "Ensure proper contrast between text and background." 40 | ) 41 | 42 | requirements.append(f"Use {aspect_ratio.value} aspect ratio.") 43 | requirements.append(f"Generate at {image_size.value} resolution.") 44 | 45 | if diagram_type == DiagramType.ARCHITECTURE: 46 | requirements.append("Use standard architecture diagram notation.") 47 | elif diagram_type == DiagramType.FLOWCHART: 48 | requirements.append("Use standard flowchart symbols.") 49 | elif diagram_type == DiagramType.SEQUENCE: 50 | requirements.append("Use standard UML sequence notation.") 51 | 52 | optimized = base_prompt.strip() 53 | if requirements: 54 | optimized += "\n\n" + " ".join(requirements) 55 | 56 | return optimized 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "blueprint-mcp" 3 | version = "3.0.0" 4 | description = "Diagram generation for codebases using Nano Banana Pro" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "google-genai>=1.0.0", 9 | "arcade-mcp-server>=0.1.0", 10 | "pillow>=10.0.0", 11 | "pydantic>=2.0.0", 12 | ] 13 | 14 | [project.optional-dependencies] 15 | dev = [ 16 | "pytest>=7.0.0", 17 | "pytest-asyncio>=0.21.0", 18 | ] 19 | 20 | [build-system] 21 | requires = ["setuptools>=61.0"] 22 | build-backend = "setuptools.build_meta" 23 | 24 | [tool.setuptools] 25 | py-modules = ["server", "generator", "prompts"] 26 | 27 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import base64 4 | import uuid 5 | import threading 6 | from pathlib import Path 7 | from typing import Annotated, Optional 8 | from datetime import datetime, timedelta 9 | from enum import Enum 10 | 11 | from arcade_mcp_server import Context, MCPApp 12 | 13 | sys.path.insert(0, str(Path(__file__).parent)) 14 | 15 | _diagram_jobs = {} 16 | MAX_JOBS_IN_MEMORY = 3 17 | JOB_EXPIRY_MINUTES = 10 18 | 19 | 20 | class JobStatus(str, Enum): 21 | QUEUED = "queued" 22 | GENERATING = "generating" 23 | COMPLETE = "complete" 24 | FAILED = "failed" 25 | 26 | 27 | def _cleanup_old_jobs(): 28 | cutoff = datetime.now() - timedelta(minutes=JOB_EXPIRY_MINUTES) 29 | expired = [jid for jid, job in _diagram_jobs.items() if job.get("created", datetime.now()) < cutoff] 30 | for jid in expired: 31 | del _diagram_jobs[jid] 32 | 33 | if len(_diagram_jobs) > MAX_JOBS_IN_MEMORY: 34 | completed = [(jid, job.get("completed", datetime.min)) for jid, job in _diagram_jobs.items() if job.get("status") == JobStatus.COMPLETE] 35 | completed.sort(key=lambda x: x[1]) 36 | for jid, _ in completed[:len(_diagram_jobs) - MAX_JOBS_IN_MEMORY]: 37 | del _diagram_jobs[jid] 38 | 39 | 40 | def _generate_diagram_background(job_id, api_key, prompt, aspect_ratio, resolution, filename_prefix, output_dir): 41 | from generator import DiagramGenerator 42 | 43 | try: 44 | _diagram_jobs[job_id]["status"] = JobStatus.GENERATING 45 | _diagram_jobs[job_id]["started"] = datetime.now() 46 | 47 | generator = DiagramGenerator(api_key=api_key, output_dir=output_dir or Path.cwd()) 48 | result = generator.generate_from_prompt(prompt, aspect_ratio, resolution, filename_prefix) 49 | 50 | if result.success: 51 | with open(result.file_path, 'rb') as f: 52 | image_bytes = f.read() 53 | image_base64 = base64.b64encode(image_bytes).decode('utf-8') 54 | 55 | Path(result.file_path).unlink() 56 | 57 | _diagram_jobs[job_id]["status"] = JobStatus.COMPLETE 58 | _diagram_jobs[job_id]["completed"] = datetime.now() 59 | _diagram_jobs[job_id]["result"] = { 60 | "success": True, 61 | "width": result.width, 62 | "height": result.height, 63 | "model": result.model_used, 64 | "filename": Path(result.file_path).name, 65 | "base64": image_base64 66 | } 67 | else: 68 | _diagram_jobs[job_id]["status"] = JobStatus.FAILED 69 | _diagram_jobs[job_id]["completed"] = datetime.now() 70 | _diagram_jobs[job_id]["result"] = {"success": False, "error": result.error} 71 | except Exception as e: 72 | _diagram_jobs[job_id]["status"] = JobStatus.FAILED 73 | _diagram_jobs[job_id]["completed"] = datetime.now() 74 | _diagram_jobs[job_id]["result"] = {"success": False, "error": str(e)} 75 | 76 | 77 | app = MCPApp(name="blueprint_mcp", version="3.0.0", log_level="INFO") 78 | 79 | 80 | @app.tool(requires_secrets=["GOOGLE_API_KEY"]) 81 | def start_diagram_job( 82 | context: Context, 83 | description: Annotated[str, "Diagram description with specific components and labels"], 84 | diagram_type: Annotated[Optional[str], "Type: architecture, flowchart, data_flow, sequence, infographic, generic"] = "generic", 85 | aspect_ratio: Annotated[Optional[str], "Ratio: 1:1, 16:9, 9:16, 4:3, 3:4, 21:9"] = "16:9", 86 | resolution: Annotated[Optional[str], "Resolution: 1K, 2K"] = "2K", 87 | output_dir: Annotated[Optional[str], "Output directory"] = None, 88 | ) -> Annotated[str, "Job ID"]: 89 | """Start diagram generation.""" 90 | from prompts import DiagramType, AspectRatio, ImageSize, optimize_prompt_for_nano_banana 91 | 92 | try: 93 | api_key = context.get_secret("GOOGLE_API_KEY") 94 | job_id = str(uuid.uuid4()) 95 | 96 | try: 97 | dtype = DiagramType(diagram_type.lower()) 98 | except ValueError: 99 | dtype = DiagramType.GENERIC 100 | 101 | optimized_prompt = optimize_prompt_for_nano_banana( 102 | description, dtype, AspectRatio(aspect_ratio), ImageSize(resolution), emphasis_on_text=True 103 | ) 104 | 105 | _cleanup_old_jobs() 106 | _diagram_jobs[job_id] = {"status": JobStatus.QUEUED, "created": datetime.now()} 107 | 108 | threading.Thread( 109 | target=_generate_diagram_background, 110 | args=(job_id, api_key, optimized_prompt, aspect_ratio, resolution, f"diagram_{diagram_type}", Path(output_dir) if output_dir else None), 111 | daemon=True 112 | ).start() 113 | 114 | return f"Job ID: {job_id}\nWait 30 seconds, then check_job_status" 115 | except Exception as e: 116 | return f"Error: {str(e)}" 117 | 118 | 119 | @app.tool 120 | def check_job_status( 121 | context: Context, 122 | job_id: Annotated[str, "Job ID"], 123 | ) -> Annotated[str, "Job status"]: 124 | """Check generation progress.""" 125 | _cleanup_old_jobs() 126 | 127 | if job_id not in _diagram_jobs: 128 | return "Job not found" 129 | 130 | job = _diagram_jobs[job_id] 131 | status = job["status"] 132 | elapsed = (datetime.now() - job["created"]).total_seconds() 133 | 134 | if status == JobStatus.COMPLETE: 135 | return f"Complete ({elapsed:.0f}s) - Ready to download" 136 | elif status == JobStatus.FAILED: 137 | return f"Failed: {job.get('result', {}).get('error', 'Unknown')}" 138 | elif status == JobStatus.GENERATING: 139 | return f"Generating ({elapsed:.0f}s elapsed, typically 30-60s)" 140 | else: 141 | return f"Queued ({elapsed:.0f}s elapsed)" 142 | 143 | 144 | @app.tool 145 | def download_diagram( 146 | context: Context, 147 | job_id: Annotated[str, "Job ID"], 148 | ) -> Annotated[str, "Base64 PNG"]: 149 | """Download diagram. Format: IMAGE|filename|width|height|base64""" 150 | _cleanup_old_jobs() 151 | 152 | if job_id not in _diagram_jobs: 153 | return "Job not found" 154 | 155 | job = _diagram_jobs[job_id] 156 | 157 | if job["status"] != JobStatus.COMPLETE: 158 | return f"Job not ready (status: {job['status']})" 159 | 160 | result = job.get("result", {}) 161 | if not result.get("success"): 162 | return f"Failed: {result.get('error')}" 163 | 164 | response = f"IMAGE|{result['filename']}|{result['width']}|{result['height']}|{result['base64']}" 165 | del _diagram_jobs[job_id] 166 | 167 | return response 168 | 169 | 170 | def main(): 171 | transport = sys.argv[1] if len(sys.argv) > 1 else "stdio" 172 | app.run(transport=transport, host="127.0.0.1", port=8000) 173 | 174 | 175 | if __name__ == "__main__": 176 | main() 177 | --------------------------------------------------------------------------------