# Architecture — mermaid-mcp This document explains the architecture of **mermaid-mcp**: how responsibilities are split, how requests flow through the system, and how the design supports safe access, predictable behavior, and easy extensibility. ## Table of contents - [Goals](#goals) - [Architecture at a glance](#architecture-at-a-glance) - [Directory map (where things live)](#directory-map-where-things-live) - [Request flow](#request-flow) - [The FileSource contract (the core idea)](#the-filesource-contract-the-core-idea) - [Source Factory (how backends are selected)](#source-factory-how-backends-are-selected) - [Component responsibilities](#component-responsibilities) - [Path and filtering semantics (stable behavior)](#path-and-filtering-semantics-stable-behavior) - [Cross-cutting policies (why they exist)](#cross-cutting-policies-why-they-exist) - [Interaction diagram (high-level)](#interaction-diagram-high-level) - [Prompts & Resources](#prompts--resources) - [Error boundaries (who raises what)](#error-boundaries-who-raises-what) - [Security boundaries and predictable behavior](#security-boundaries-and-predictable-behavior) - [Extending the system: adding a new source](#extending-the-system-adding-a-new-source) - [Design tradeoffs](#design-tradeoffs) --- ## Goals - **Backend-agnostic tools**: tools should not depend on whether files come from a local folder or a remote repository. - **Contract-driven design**: one clear contract defines what a “source” is and how it behaves. - **Predictable runtime behavior**: bounded reads, safe output paths, and stable performance under concurrency. - **Extensibility**: adding new sources should not require changing tools. --- ## Architecture at a glance The server exposes three MCP tools: - `list_files` — discover files in a source - `read_file` — read file contents with a configured limit - `render_mermaid` — render Mermaid → PNG via Kroki and save output The system is organized into four layers: 1. **Tools** — MCP surface and orchestration 2. **Sources** — backends that implement the Source contract 3. **Clients** — thin wrappers around external services (GitHub/Kroki) 4. **Core** — shared contracts and cross-cutting primitives --- ## Directory map (where things live) ```text src/ server/ # MCP server entrypoint and registration tools/ # MCP tools (list_files / read_file / render_mermaid) sources/ # Source implementations + source factory clients/ # External service wrappers (GitHub / Kroki) core/ # Shared contracts, errors, models, policies, and path semantics (paths.py) prompts/ # Server-registered prompts used to generate Mermaid (src/prompts/) resources/ # Packaged static resources (Mermaid styles, templates) (src/resources/) ``` --- ## Request flow A typical end-to-end pipeline: 1. The client/agent calls `list_files`. 2. The tool selects a backend source (Local / GitHub) via the factory. 3. The tool calls `source.list_files(...)`. 4. The client/agent selects a small set of files and calls `read_file`. 5. The tool calls `source.read_file(...)`. 6. The client/agent generates Mermaid from real content. 7. The client/agent calls `render_mermaid` to produce PNG output. --- ## The FileSource contract (the core idea) The `FileSource` contract is the interface that all backends must implement. Tools interact with sources only through this contract, which keeps tools backend-agnostic and makes new backends pluggable without changing tools. ### Contract surface The `FileSource` instance is created by the factory with any backend configuration baked in (e.g., `project_root` for local, `repo_url`/`ref` for GitHub). The tool layer does not perform backend-specific logic beyond selecting/instantiating the source. Pseudo-signature (conceptual): ```python class FileSource: async def list_files( self, *, root: str = ".", glob: str = "**/*", recursive: bool = True, ) -> list[str]: ... async def read_file( self, *, path: str, max_chars: int, ) -> str: ... ``` ### Contract guarantees All `FileSource` implementations must guarantee: - Normalized paths in results: `list_files(...)` returns paths in a stable, normalized form (recommended: forward slashes `/`, no leading `./`, no trailing slashes). - Relative-to-source semantics: `root` and `path` are interpreted relative to the source’s base (local project root or remote repo root). - Bounded reads: `read_file(...)` respects `max_chars` and never returns unbounded content. - Consistent failure modes: - missing file/path → `NotFoundError` (project-level error) - invalid inputs (bad URL / illegal path) → `ValidationError` - upstream issues (network, throttling after retries, API errors) → `ExternalServiceError` This contract makes new sources pluggable without changing tools. --- ## Source Factory (how backends are selected) The factory keeps tools simple and consistent. ### Selection inputs (tool parameters) At the MCP boundary the tool typically receives: - `source`: "local" or "github" For local: `root`, `glob`, `recursive`, etc. For github: `repo_url` (required), `ref` (optional; the source/client may apply sensible defaults). ### Factory behavior (conceptual) - If `source == "local"`: create a `LocalSource(project_root=PROJECT_ROOT, ...)`. - If `source == "github"`: validate `repo_url` and create a `GitHubSource(repo_url=..., ref=..., client=GitHubClient(...), ...)`. Key rule: backend-specific setup belongs in the factory and source constructors, not in tools. --- ## Component responsibilities 1) Tools (orchestration layer) What belongs here: - Input validation at the MCP boundary (required params, type/shape checks). - Selecting the correct source via the factory. - Returning results in MCP-friendly formats. - For `render_mermaid`: output filename sanitization and writing within the allowed output directory. What does NOT belong here: - Filesystem containment checks. - Remote API specifics (headers, pagination, ref quirks). - Cross-cutting policies (caching, pacing, rate-limit handling). 2) Sources (backend implementations) Sources implement the `FileSource` contract and own backend-specific concerns. Local source: - Enforces the `PROJECT_ROOT` boundary (no reads outside). - Normalizes paths and performs containment checks. - Applies `MAX_FILE_CHARS` to keep reads bounded. Remote repository source (GitHub today; others later): - Translates Source operations into remote API calls. - Handles ref resolution behavior (including default-branch fallback when appropriate). - Delegates network concerns to a dedicated client wrapper. 3) Clients (external service wrappers) Clients are deliberately thin. They should: - Encapsulate HTTP details (headers, timeouts, response parsing). - Translate service errors into project-level errors. - Remain reusable by sources without leaking service-specific quirks. Kroki client: - Sends Mermaid text and receives PNG bytes. - Returns bytes and lets the tool decide output behavior (save path, filename, overwrite policy). Remote repo client: - Supports listing and reading with consistent normalization and failure modes. - Keeps higher layers clean from API mechanics. 4) Core (shared primitives and policies) Core hosts reusable building blocks used across the project: - `errors` — project-specific exception types - `models` — shared data structures - `paths` — shared path normalization + glob semantics incl. `**` - policy primitives (caching / pacing / rate limiting) used by networked components Core is where "how we behave" lives, preventing behavioral drift across modules. --- ## Path and filtering semantics (stable behavior) To keep behavior predictable across sources: - `root` is a directory-like prefix to scope listing (default `.`). - `glob` is applied to the candidate file set (default `**/*`). - `recursive` controls directory traversal depth for backends that support it. Recommended rule: tools apply the same parameter meanings for both local and remote. If a backend cannot support a parameter precisely, it should approximate reasonably but keep the same output invariants (normalized paths, stable ordering if applicable). Implementation note: shared path normalization and `glob`/`**` matching are centralized in `core/paths.py` to prevent behavioral drift. --- ## Cross-cutting policies (why they exist) These policies are runtime safety and stability mechanisms, not service-specific features: - Bounded reads: prevents runaway memory/time on large files (`MAX_FILE_CHARS`). - Concurrency cap: prevents too many simultaneous network calls and reduces contention. - Pacing: avoids burst traffic when many tasks run concurrently. - Rate-limit handling: respects explicit server throttling signals for predictable retries. - TTL caching: avoids repeating identical remote reads within short windows. Outcome: stable tool latency and fewer avoidable failures under load. --- ## Interaction diagram (high-level) ```mermaid flowchart LR A[Client / Agent] -->|list_files| T1[Tool: list_files] A -->|read_file| T2[Tool: read_file] A -->|render_mermaid| T3[Tool: render_mermaid] T1 --> F[Source Factory] T2 --> F F --> L[Local Source] F --> R[Remote Repo Source] L --> FS[(Filesystem)] R --> RC[Remote Repo Client] RC --> API[(Remote API)] T3 --> KC[Kroki Client] KC --> K[(Kroki)] T3 --> OUT[(PNG bytes + saved file)] ``` --- ## Prompts & Resources This project includes two supporting concepts that are important for agent-driven workflows and consistent Mermaid generation: server-registered prompts and packaged resources. Prompts - The server exposes canonical prompts that agents/clients can use when generating Mermaid diagrams. The canonical prompt enforces the pipeline (list_files → read_file → render_mermaid), style rules, and requirements such as embedding a style resource unchanged. - Location: `src/prompts/` (the main prompt is `src/prompts/mermaid_prompt.py`, registered under the name `generate_mermaid_canonical`). - Usage: MCP clients that support server-side prompts can select `generate_mermaid_canonical` so agents receive a stable, curated instruction set. If a client cannot use server-side prompts, copy the prompt text from `src/prompts/mermaid_prompt.py` and keep local copies in sync with repository changes. - Rationale: keeping a canonical prompt on the server ensures consistency across agents and reduces errors from ad-hoc prompt variants. Resources - The project bundles static resources used by prompts and Mermaid diagrams (for example, color/style templates). - Location: `src/resources/` (e.g., `mermaid_style_blue_flowchart.mmd` and `mermaid_styles.py`). - Access: prompts expect resources to be available via the resource URI scheme used by the prompt (for example `mermaid://styles/blue-flowchart`). Tools or prompts that require a resource should read it from `src/resources/` and embed it into generated Mermaid text unchanged. - Rationale: shipping canonical styles and other small assets with the server ensures diagrams are visually consistent and that agents do not rely on external resources at render time. Operational notes - When updating prompts or resources, update tests and documentation to avoid breaking agents that rely on the canonical prompt or style names. - Prompts can reference resource names; if a resource filename or name changes, update both the prompt and any code that resolves resource URIs. --- ## Error boundaries (who raises what) Tools: - Validate presence/shape of required parameters and raise `ValidationError` early for malformed tool calls. Sources: - Enforce source boundaries (e.g., local containment) and translate "not found" into `NotFoundError`. - Propagate project-level errors upward (not raw HTTP/filesystem exceptions). Clients: - Translate HTTP/network failures into `ExternalServiceError`. - Handle retryable scenarios internally (rate limit / transient errors) within a bounded retry policy. Guiding principle: callers should never need to interpret raw HTTP status codes or OS errors. --- ## Security boundaries and predictable behavior - Local reads are restricted to `PROJECT_ROOT` (path traversal is rejected). - Output writes are restricted to `DIAGRAM_OUT_DIR` within `PROJECT_ROOT`. - Reads are bounded by `MAX_FILE_CHARS` to prevent oversized payloads. - Output filenames derived from `title` are sanitized to be filesystem-safe. - Network behavior is stabilized via pacing, concurrency caps, and rate-limit handling. --- ## Extending the system: adding a new source To add a new backend (e.g., GitLab / Bitbucket / ZIP / URL): 1. Implement the Source contract in a new source module. 2. Register it in the factory (selection by `source=...`). 3. Add tests for list/read behavior and boundary conditions. 4. Update docs (README + this architecture doc if needed). Key point: tools should remain unchanged. --- ## Design tradeoffs - Contract-first over tool-specific logic: enables clean extensibility. - Thin clients: keeps service specifics out of tools and sources. - In-memory TTL caching: fast, simple, and sufficient for a stdio server (not persistent across runs). - Limited retries: keeps tool calls responsive and avoids retry storms.