How an MCP-speaking assistant mounts this site's documentation as a resource and reads it natively — no scraping, no copying, no context window spent on HTML.

MCP Clients

How an MCP-speaking assistant mounts this site's documentation as a resource and reads it natively — no scraping, no copying, no context window spent on HTML.


The audience

An MCP client is an assistant that speaks the Model Context Protocol — Claude Desktop, an IDE extension, an agent framework. It does not browse. It asks a server for resources and reads what comes back.

A Dash app has nothing to offer such a client by default. The HTML is a loading shell, and the content only exists after React has run. So the client either gets nothing, or burns a large fraction of its context window on markup that carries none of your meaning.

dash-improve-my-llms closes that in one line. On Dash 4.3+, every page that registers prose becomes an MCP resource automatically:

add_llms_routes(app, LLMSConfig(register_mcp_resources=True))  # the default

What a client sees

Pick a page and read exactly what this site hands an MCP client for it. The list is this app's own live page registry — not a fixture.

# Live component, rendered above on the browser lane.
# Source: docs/mcp_clients/mcp_registry.py

"""Showcase exec module: what an MCP client actually mounts from this site.

Two exec-module rules apply here and are load-bearing enough to restate:

1. **Globally-unique id prefix.** Every id in this module starts `mcpx-`.
   Dash ids share one namespace across ~45 exec modules on this site; a
   collision does not error, it silently wires the wrong callback.
2. **No import-time registry walk.** `dash.page_registry` is INCOMPLETE while
   `pages/markdown.py` is still globbing — this module is imported from
   inside that loop. So `component` below is a placeholder and the table is
   populated by a callback, which runs long after registration finishes.
"""
from dash import Input, Output, callback, dcc, html, no_update
import dash_mantine_components as dmc
from dash_iconify import DashIconify

ID = "mcpx"

component = html.Div(
    [
        dmc.Group(
            [
                dmc.Select(
                    id=f"{ID}-page",
                    label="Page",
                    description="Every page this site registers is an MCP resource.",
                    placeholder="Loading the registry…",
                    searchable=True,
                    w=340,
                ),
                dmc.Switch(
                    id=f"{ID}-raw",
                    label="Show the raw resource body",
                    checked=False,
                    mt=28,
                ),
            ],
            gap="lg",
            align="flex-start",
        ),
        dmc.Space(h="md"),
        dmc.Paper(
            dmc.Stack(
                [
                    dmc.Group(
                        [
                            DashIconify(icon="tabler:plug-connected", width=18),
                            dmc.Text("Resource URI", size="sm", fw=600),
                        ],
                        gap="xs",
                    ),
                    dmc.Code(id=f"{ID}-uri", block=True),
                    dmc.Text(id=f"{ID}-hint", size="xs", c="dimmed"),
                ],
                gap="xs",
            ),
            withBorder=True,
            radius="md",
            p="md",
        ),
        dmc.Space(h="md"),
        html.Div(id=f"{ID}-body"),
        dcc.Interval(id=f"{ID}-boot", interval=200, max_intervals=1),
    ]
)


@callback(
    Output(f"{ID}-page", "data"),
    Output(f"{ID}-page", "value"),
    Input(f"{ID}-boot", "n_intervals"),
)
def _populate(_tick):
    """Read the registry HERE, not at import time.

    A one-shot Interval rather than a layout-time walk: by the time this
    fires, every page in docs/ has registered and the list is complete.
    """
    import dash

    options = sorted(
        (
            {"value": entry["path"], "label": entry.get("name") or entry["path"]}
            for entry in dash.page_registry.values()
            if not entry["path"].startswith("/admin/")
        ),
        key=lambda option: option["label"],
    )
    if not options:
        return no_update, no_update
    return options, options[0]["value"]


@callback(
    Output(f"{ID}-uri", "children"),
    Output(f"{ID}-hint", "children"),
    Output(f"{ID}-body", "children"),
    Input(f"{ID}-page", "value"),
    Input(f"{ID}-raw", "checked"),
)
def _describe(path, raw):
    if not path:
        return "—", "Pick a page.", None

    from lib.constants import BASE_URL

    doc = _llms_doc_for(path)
    suffix = "llms.txt" if path == "/" else f"{path.strip('/')}/llms.txt"
    uri = f"{BASE_URL}/{suffix}"

    hint = (
        "An MCP client mounts this as a resource and reads it directly. "
        "A browser opening the same URL gets it rendered instead — the route "
        "content-negotiates on Accept, and sends Vary: Accept so a CDN cannot "
        "hand cached HTML to the next agent."
    )

    if not doc:
        body = dmc.Alert(
            "This page registers no LLMS_DOC, so the resource carries its "
            "generated summary rather than hand-written prose.",
            color="yellow",
            variant="light",
            icon=DashIconify(icon="tabler:info-circle"),
        )
    elif raw:
        body = dmc.Paper(
            dmc.Code(doc[:4000] + ("\n…" if len(doc) > 4000 else ""), block=True),
            withBorder=True, radius="md", p="sm",
        )
    else:
        body = dmc.Paper(dcc.Markdown(doc[:4000]), withBorder=True, radius="md", p="md")

    return uri, hint, body


def _llms_doc_for(path):
    """The prose dash-improve-my-llms would serve for `path`.

    Read through the package's own registry so this demo cannot drift from
    what the route actually returns.
    """
    try:
        import dash_improve_my_llms as dimll

        store = getattr(getattr(dimll, "_state", None), "page_metadata", None) or {}
        return (store.get(path) or {}).get("llms_doc") or ""
    except Exception:
        # Private state, read defensively: if the package moves it, this demo
        # degrades to "no prose registered" rather than breaking the page.
        return ""

The three ways to give a page prose

1. A module-level LLMS_DOC string. The package picks it up automatically. No layout walking, no extraction heuristics.

# pages/pricing.py
LLMS_DOC = """
# Pricing

Three tiers. The free tier has no time limit.
"""

2. Markdown-driven pages. This site's docs are markdown files; the loader expands the directives and registers the result, so /<page>/llms.txt serves the expanded prose rather than the source.

3. register_page_metadata(...) for anything that is not a page — the home page, a pseudo-path, a document assembled at boot.

Content negotiation, and why Vary matters

/<page>/llms.txt serves the same URL two ways:

ClientSendsGets
agent / MCP clientno Accept: text/htmlthe Markdown, byte for byte
browserAccept: text/html,…the rendered viewer

?raw=1 and ?format=html force either side. Both variants send Vary: Accept, which is what stops a CDN handing an agent the HTML it cached for the last human.

The tiered corpus

One page at a time is often not what an agent wants. Three documents cover the other shapes:

DocumentFor
/llms.txtthe index — every page, plus the cross-host network directory
/llms-small.txta compact briefing, when the full corpus is too much
/llms-full.txteverything, in one fetch

Prefer one /llms-full.txt fetch over N per-page fetches. That is not politeness — since 2.7.0 it is the published rate contract, and the origin answers 429 with a Retry-After when an agent ignores it.

Verifying it works

# what an agent gets
curl -s https://llms.2plot.dev/audiences/mcp-clients/llms.txt | head

# what a browser gets from the same URL
curl -s -H 'Accept: text/html' https://llms.2plot.dev/audiences/mcp-clients/llms.txt | head

If the first command returns HTML, the negotiation is broken — that is the one check worth putting in your deploy pipeline.


Source: /audiences/mcp-clients

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: