5. Building Block View

This chapter describes the static decomposition of the system into its key building blocks. We use the C4 model to illustrate the structure at different levels of detail.

5.1 Level 2: System Containers

The MCP Documentation Server system is composed of two main containers: a web-based user interface and the back-end API server. The file system acts as the system’s database.

container overview

5.2 Level 3: Components of the MCP API Server

We now zoom into the MCP API Server container. It is composed of several components, each with a distinct responsibility, reflecting a classic layered architecture.

component detail api

Note: The diagram above shows the initial design. Section 5.3 documents the actual implemented modular architecture (Oct 2025) based on ADR-006.

5.3 Modular MCP Server Architecture (Actual Implementation)

Following the refactoring documented in ADR-006, the MCP API Server was split into focused modules to comply with the <500 lines constraint and improve maintainability. This section describes the actual implemented architecture as of October 2025.

Architectural Overview

The MCP Server follows an Extract-and-Delegate pattern with Dependency Injection:

modular architecture

Module Responsibilities

Module Responsibility Key Methods Lines

mcp_server.py

Server orchestration, MCP tool registration

init(), cleanup(), @mcp.tool() decorators (10 tools), delegation methods

290

document_api.py

All document operations

get_structure(), get_section(), search_content(), get_metadata(), update_section_content(), insert_section()

435

FastMCP SDK

MCP protocol handling (external dependency)

FastMCP(), mcp.run(), automatic schema generation via type hints

mcp[cli]>=1.0.0

webserver_manager.py

Web server lifecycle

find_free_port(), start_webserver_thread(), get_webserver_status()

121

document_parser.py

Parsing logic

parse_file(), resolve_includes()

82

content_editor.py

File modifications

update_section(), atomic writes

46

file_watcher.py

File system monitoring

start(), _on_modified()

64

Total: 1,229 lines across 7 focused modules (vs 916 lines in monolithic mcp_server.py)

Search Relevance Algorithm

The document_api.py module implements a search relevance scoring algorithm used to rank search results returned by the search_content() method.

Algorithm Implementation: The relevance scoring is calculated in the _calculate_relevance() method using the following logic:

def _calculate_relevance(self, section: Section, query: str) -> float:
    """Simple relevance scoring"""
    title_matches = section.title.lower().count(query)
    content_matches = section.content.lower().count(query)
    return title_matches * 2 + content_matches

Scoring Logic: - Title matches: Weighted with factor 2 (higher importance) - Content matches: Weighted with factor 1 (standard importance) - Total score: Sum of weighted matches

Usage Context: This algorithm is used within the search functionality to provide more relevant results by prioritizing documents where search terms appear in titles over those where terms only appear in content.

Example: - Document with 1 title match + 3 content matches: Score = (1 × 2) + (3 × 1) = 5 - Document with 0 title matches + 5 content matches: Score = (0 × 2) + (5 × 1) = 5 - The first document would be considered more relevant due to the title match

Dependency Injection Pattern

The orchestrator (MCPDocumentationServer) creates and injects dependencies:

class MCPDocumentationServer:
    def __init__(self, project_root: Path, enable_webserver: bool = True):
        # Core components
        self.parser = DocumentParser()
        self.editor = ContentEditor(project_root)
        self.diff_engine = DiffEngine()

        # Shared state
        self.sections = {}  # In-memory index
        self.root_files = []
        self.included_files = set()

        # Modular components (dependency injection)
        self.doc_api = DocumentAPI(self)  # Receives server instance
        self.webserver = WebserverManager(self)

        # Initialize
        self._discover_root_files()
        self._parse_project()
        self.file_watcher = FileWatcher(project_root, self._on_files_changed)

Each module receives self (the server instance) to access shared state:

class DocumentAPI:
    def __init__(self, server: 'MCPDocumentationServer'):
        self.server = server  # Access to sections, parser, editor

    def get_structure(self, max_depth: int = 3):
        # Accesses self.server.sections
        return self._build_hierarchy(self.server.sections, max_depth)

Mental Model: "Modules are pure logic, orchestrator holds state"

This pattern avoids circular dependencies while maintaining clear ownership.

Module Interactions

Typical MCP Request Flow (FastMCP SDK):

  1. MCP Client → sends JSON-RPC request via stdin

  2. FastMCP SDKmcp.run() receives and parses request

  3. FastMCP SDK → routes to decorated tool (e.g., @mcp.tool() def get_structure())

  4. Tool Function → accesses _server.doc_api.get_structure() (global instance)

  5. document_api.py → executes business logic, accesses self.server.sections (shared state)

  6. FastMCP SDK → automatically serializes return value to JSON-RPC response

  7. MCP Client → receives JSON-RPC response via stdout

File Modification Flow:

  1. DocumentAPIupdate_section_content(path, content)

  2. DocumentAPI → calls self.server.editor.update_section()

  3. ContentEditor → atomic write via backup-and-replace (ADR-004)

  4. FileWatcher → detects change

  5. MCPDocumentationServer_on_files_changed() → re-parses

  6. Sections Index → updated with new content

Design Rationale (Mental Model)

Why this modular split? (See ADR-006 for full rationale)

  1. Cognitive Load Management

    • Mental Model: "One module = one mental context"

    • 500 lines ≈ maximum cognitive capacity for understanding a file

    • Each module can be understood independently

  2. Testability

    • Each module testable in isolation

    • Result: 82% coverage (vs ~50% before modularization)

  3. Parallel Development

    • Different concerns = different modules

    • Reduced merge conflicts

  4. Clear Ownership

    • Document operations → document_api.py

    • Protocol concerns → FastMCP SDK (external dependency, ADR-009)

    • Web server → webserver_manager.py

    • No ambiguity about "where does this code go?"

Trade-off: Delegation adds minor indirection overhead Justification: Clarity gain >>> performance cost

5.4 Data Structures

This section documents the core data structures that represent the document model.

Section (Document Node)

The fundamental unit of the document hierarchy:

@dataclass
class Section:
    """Represents a logical section in the documentation"""

    id: str              # Hierarchical path, e.g., "chapter-1.section-2"
    title: str           # Section title (from heading)
    content: str         # Text content of this section
    level: int           # Heading level (1=chapter, 2=section, 3=subsection, etc.)
    children: List[str]  # IDs of child sections (hierarchical structure)
    source_file: str     # Path to source .adoc/.md file
    line_start: int      # Start line in source file (1-indexed)
    line_end: int        # End line in source file (inclusive)

Mental Model: "A Section is a logical chunk, not a file chunk"

Key insights: - id encodes hierarchy: "chapter-1.section-2.subsection-3" - source_file + line_start/line_end enable precise file editing - Multiple sections can come from one file (via includes) - One section’s content can span multiple files (via includes)

Example:

docs/architecture.adoc (lines 1-100):
  Section(id="architecture-documentation", level=1, line_start=1, line_end=2)
  Section(id="architecture-documentation.introduction", level=2, line_start=3, line_end=10)

_introduction.adoc (lines 1-50) [included by architecture.adoc]:
  Section(id="architecture-documentation.introduction.goals", level=3, line_start=1, line_end=20)

Structure Index (In-Memory)

The server maintains an in-memory index for O(1) lookups:

class MCPDocumentationServer:
    sections: Dict[str, Section]  # id → Section mapping
    root_files: List[Path]        # Files not included by others
    included_files: Set[Path]     # Files included by others

Performance: - Lookup by ID: O(1) - All sections at level N: O(n) linear scan - Search by query: O(n) with early termination

Memory: - ~600 pages ≈ ~1000 sections - ~1000 sections × ~1KB/section ≈ 1MB in-memory - Acceptable trade-off for instant access

Include Graph

Tracked implicitly via source_file and included_files:

root_files = [main.adoc, other.adoc]
included_files = [_intro.adoc, _glossary.adoc]

Logical structure:
  main.adoc
    ├── Section from main.adoc
    ├── Section from _intro.adoc (included)
    └── Section from _glossary.adoc (included)

Mental Model: "Includes are flattened during parsing, tracked for navigation"

The parser resolves includes recursively, flattening the logical document tree while preserving file provenance for editing.