Skip to content

Commit 82bf1e4

Browse files
feat: add Markdown MCP docs generator (Docusaurus- and pdoc-compatible) (#1015)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent ce1a589 commit 82bf1e4

10 files changed

Lines changed: 781 additions & 3 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ viztracer_report.json
1111
# Packaged docs
1212
docs/*.zip
1313

14+
# Generated MCP server docs (regenerate via `poe mcp-docs-md`)
15+
docs/mcp-generated/
16+
1417
# Misc
1518
.DS_Store
1619

airbyte/mcp/cloud.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2-
"""Airbyte Cloud MCP operations."""
2+
"""Airbyte Cloud MCP operations.
3+
4+
.. include:: ../../docs/mcp-generated/cloud.md
5+
"""
6+
7+
# No public Python API — MCP primitives are registered via decorators and
8+
# documented via the generated Markdown include above. Setting `__all__` to an
9+
# empty list tells pdoc (and other doc tools) not to surface the individual
10+
# tool / helper definitions as a redundant "API Documentation" list.
11+
__all__: list[str] = []
312

413
from pathlib import Path
514
from typing import Annotated, Any, Literal, cast

airbyte/mcp/local.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2-
"""Local MCP operations."""
2+
"""Local MCP operations.
3+
4+
.. include:: ../../docs/mcp-generated/local.md
5+
"""
6+
7+
# No public Python API — MCP primitives are registered via decorators and
8+
# documented via the generated Markdown include above. Setting `__all__` to an
9+
# empty list tells pdoc (and other doc tools) not to surface the individual
10+
# tool / helper definitions as a redundant "API Documentation" list.
11+
__all__: list[str] = []
312

413
import sys
514
import traceback

airbyte/mcp/prompts.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
This module defines prompts that can be invoked by MCP clients to perform
55
common workflows.
6+
7+
.. include:: ../../docs/mcp-generated/prompts.md
68
"""
79

810
from __future__ import annotations
@@ -13,6 +15,13 @@
1315
from pydantic import Field
1416

1517

18+
# No public Python API — MCP primitives are registered via decorators and
19+
# documented via the generated Markdown include above. Setting `__all__` to an
20+
# empty list tells pdoc (and other doc tools) not to surface the individual
21+
# tool / helper definitions as a redundant "API Documentation" list.
22+
__all__: list[str] = []
23+
24+
1625
if TYPE_CHECKING:
1726
from fastmcp import FastMCP
1827

airbyte/mcp/registry.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2-
"""Airbyte Cloud MCP operations."""
2+
"""Airbyte connector registry MCP operations.
3+
4+
.. include:: ../../docs/mcp-generated/registry.md
5+
"""
6+
7+
# No public Python API — MCP primitives are registered via decorators and
8+
# documented via the generated Markdown include above. Setting `__all__` to an
9+
# empty list tells pdoc (and other doc tools) not to surface the individual
10+
# tool / helper definitions as a redundant "API Documentation" list.
11+
__all__: list[str] = []
312

413
# Note: Deferred type evaluation must be avoided due to FastMCP/Pydantic needing
514
# types to be available at import time for tool registration.

docs/CONTRIBUTING.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,36 @@ poe mcp-serve-sse # Server-Sent Events transport on localhost:8000
143143

144144
poe mcp-inspect # Show all available MCP tools and their schemas
145145
```
146+
147+
### Generating Markdown docs for the MCP Server
148+
149+
The repo ships a small script (`scripts/generate_mcp_markdown.py`) that
150+
introspects the MCP server via `fastmcp inspect` and emits a Markdown
151+
documentation site under `docs/mcp-generated/` (git-ignored). The output is
152+
plain CommonMark with no MDX-only components, so it is both Docusaurus-hostable
153+
and consumable by `pdoc` — the four `airbyte.mcp.{cloud,local,registry,prompts}`
154+
modules pull their respective generated file in via pdoc's `.. include::`
155+
directive, so `poe docs-generate` surfaces the generated tool docs on each
156+
module's pdoc page alongside the regular `docs/generated/` output.
157+
158+
```bash
159+
uv sync --group dev
160+
poe mcp-docs-md
161+
```
162+
163+
One Markdown file is produced per MCP module, plus an `index.md`. For the
164+
PyAirbyte server that is:
165+
166+
- `index.md` — server overview (name, version, instructions, totals, module table)
167+
- `cloud.md` — tools registered by `airbyte.mcp.cloud`
168+
- `local.md` — tools registered by `airbyte.mcp.local`
169+
- `registry.md` — tools registered by `airbyte.mcp.registry`
170+
- `prompts.md` — prompts registered by `airbyte.mcp.prompts`
171+
- `misc.md` — anything without an `mcp_module` annotation (currently just the
172+
`server_info` resource)
173+
174+
Inside each module page, primitives are grouped by kind (`## Tools`,
175+
`## Prompts`, `## Resources`), and each primitive has an HTML anchor
176+
(`<a id="name"></a>`) above its H3 so links like
177+
`cloud.md#deploy_source_to_cloud` resolve in both pdoc and Docusaurus.
178+
Regenerate after any change to MCP tool signatures, descriptions, or schemas.

docs/generate.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,81 @@
1010

1111
from __future__ import annotations
1212

13+
import importlib.util
1314
import pathlib
1415
import shutil
16+
import sys
1517

1618
import pdoc
19+
import pdoc.render_helpers
20+
21+
22+
def _regenerate_mcp_markdown() -> None:
23+
"""Regenerate `docs/mcp-generated/` before pdoc runs.
24+
25+
The `airbyte.mcp.{cloud,local,registry,prompts}` modules pull the
26+
per-module Markdown files from `docs/mcp-generated/` via pdoc's
27+
`.. include::` directive. That directory is git-ignored, so on a clean
28+
checkout pdoc would fail to resolve the include unless we regenerate it
29+
here. Running the generator from inside `docs-generate` makes the full
30+
docs build reproducible from a fresh clone (and matches the standalone
31+
`poe mcp-docs-md` task).
32+
33+
If generation fails (e.g. `fastmcp` is not installed, or the MCP server
34+
import fails), we print a warning and continue: pdoc will still build,
35+
and the include directive will just surface the missing file.
36+
37+
We load the generator via `importlib.util` from its on-disk path rather
38+
than a plain `from generate_mcp_markdown import ...`: the generator
39+
lives under `scripts/` (not on `sys.path`), and a static import would
40+
also trip `deptry` into flagging `generate_mcp_markdown` as a missing
41+
external dependency.
42+
"""
43+
script = pathlib.Path(__file__).parent.parent / "scripts" / "generate_mcp_markdown.py"
44+
if not script.exists():
45+
print(f"[docs-generate] MCP markdown generator not found at {script}; skipping.")
46+
return
47+
try:
48+
spec = importlib.util.spec_from_file_location("_mcp_markdown_gen", script)
49+
if spec is None or spec.loader is None:
50+
msg = f"Could not load spec for {script}"
51+
raise RuntimeError(msg) # noqa: TRY301
52+
module = importlib.util.module_from_spec(spec)
53+
spec.loader.exec_module(module)
54+
print("[docs-generate] Regenerating docs/mcp-generated/ ...")
55+
module.generate(
56+
server_spec=module.DEFAULT_SERVER_SPEC,
57+
output=module.DEFAULT_OUTPUT,
58+
)
59+
except Exception as ex:
60+
print(
61+
f"[docs-generate] WARNING: failed to regenerate MCP Markdown docs: {ex}. "
62+
"pdoc will continue, but module pages may show missing include warnings.",
63+
file=sys.stderr,
64+
)
1765

1866

1967
def run() -> None:
2068
"""Generate docs for all public modules in PyAirbyte and save them to docs/generated."""
2169
public_modules = ["airbyte", "airbyte/cli/pyab.py"]
2270

71+
# Regenerate MCP Markdown first so the `.. include::` directives in the
72+
# MCP module docstrings resolve on a clean checkout (docs/mcp-generated/
73+
# is git-ignored).
74+
_regenerate_mcp_markdown()
75+
2376
# recursively delete the docs/generated folder if it exists
2477
if pathlib.Path("docs/generated").exists():
2578
shutil.rmtree("docs/generated")
2679

80+
# pdoc's default sidebar TOC depth is 2 (H1 + H2 only), which hides the
81+
# per-tool H3 anchors produced by our MCP Markdown generator. Bump to 3 so
82+
# individual tools / prompts / resources show up in the left nav. This
83+
# monkey-patches the module-level `markdown_extensions` dict because pdoc
84+
# 16's `configure()` does not expose markdown extension options.
85+
# pyrefly: ignore[unsupported-operation]
86+
pdoc.render_helpers.markdown_extensions["toc"] = {"depth": 3}
87+
2788
pdoc.render.configure(
2889
template_directory=pathlib.Path("docs/templates"),
2990
show_source=True,

docs/templates/custom.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,22 @@ nav a:hover {
174174
color: var(--link-hover) !important;
175175
}
176176

177+
/*
178+
* Progressively indent nested TOC levels in the sidebar.
179+
*
180+
* pdoc's default layout.css indents *all* non-top-level nav items by a single
181+
* (pad + indent) step, which makes H2 and H3 entries render at the same visual
182+
* depth. When the generated MCP Markdown uses H3 headings per-tool nested
183+
* under an H2 "Tools" heading, we want the tool names to appear visibly
184+
* nested under the section heading in the left nav.
185+
*/
186+
nav.pdoc > div > ul > li > ul > li > ul > li > a {
187+
padding-left: calc(var(--pad) + (var(--indent) * 2)) !important;
188+
}
189+
nav.pdoc > div > ul > li > ul > li > ul > li > ul > li > a {
190+
padding-left: calc(var(--pad) + (var(--indent) * 3)) !important;
191+
}
192+
177193
/* Style badges and labels */
178194
.badge {
179195
background-color: var(--color-green-40);

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ mcp-serve-http = { cmd = "python -c \"from airbyte.mcp.server import app; app.ru
174174
mcp-serve-sse = { cmd = "python -c \"from airbyte.mcp.server import app; app.run(transport='sse', host='127.0.0.1', port=8000)\"", help = "Start the MCP server with SSE transport" }
175175
mcp-inspect = { cmd = "fastmcp inspect airbyte/mcp/server.py:app", help = "Inspect MCP tools and resources (supports --tools, --health, etc.)" }
176176
mcp-tool-test = { cmd = "python -m fastmcp_extensions.utils.test_tool --app airbyte.mcp.server:app", help = "Test MCP tools directly with JSON arguments: poe mcp-tool-test <tool_name> '<json_args>'" }
177+
mcp-docs-md = { cmd = "python scripts/generate_mcp_markdown.py", help = "Generate Markdown docs for the MCP server into docs/mcp-generated/ (Docusaurus- and pdoc-compatible)" }
177178

178179
# Claude Code MCP Testing Tasks
179180
[tool.poe.tasks.test-my-tools]

0 commit comments

Comments
 (0)