1515import shutil
1616from pathlib import Path
1717
18+ import griffe
19+
1820# A MkDocs/Zensical nav is a list of entries, each either ``{title: url}`` for a
1921# page or ``{title: [children]}`` for a section (a bare ``url`` string attaches
2022# a section index page, courtesy of the ``navigation.indexes`` feature).
2325ROOT = Path (__file__ ).parent .parent .parent
2426API_DIR = ROOT / "docs" / "api"
2527
28+ # `src/mcp-types` is a distribution directory, not an import package, so each
29+ # package's dotted module path is taken relative to its own parent: deriving
30+ # it from `src/` would emit the unimportable `mcp-types.mcp_types.*`.
31+ PACKAGES = (ROOT / "src" / "mcp" , ROOT / "src" / "mcp-types" / "mcp_types" )
32+
33+ _KIND_SECTIONS = {
34+ griffe .Kind .MODULE : "Modules" ,
35+ griffe .Kind .CLASS : "Classes" ,
36+ griffe .Kind .FUNCTION : "Functions" ,
37+ griffe .Kind .ATTRIBUTE : "Attributes" ,
38+ griffe .Kind .TYPE_ALIAS : "Type aliases" ,
39+ }
40+
2641
2742class _Node :
2843 """A module (``url``) and/or a package with child modules (``children``)."""
@@ -45,18 +60,71 @@ def to_nav(self, title: str) -> NavItem:
4560 return {title : items }
4661
4762
63+ def _compact_index (package : griffe .Module , documented : set [str ]) -> str | None :
64+ """Build a compact index body for a package that re-exports another package's API.
65+
66+ mkdocstrings renders a member re-exported across a package boundary
67+ (``from other_package import y`` + ``__all__``) as a full duplicate of its
68+ canonical documentation whenever the other package happens to be loaded
69+ already, and silently omits it when it isn't — which of the two a package
70+ index gets depends on page rendering order. Same-package re-exports
71+ (``mcp_types`` re-exporting its private ``._types`` module) always resolve
72+ and are unaffected, so such packages keep the plain ``::: package`` stub
73+ (return ``None``) and their index remains the full, canonical rendering.
74+
75+ For an affected package, pin the semantics instead of inheriting the
76+ accident: every export whose canonical page exists elsewhere under the API
77+ reference becomes a link to it, and only exports documented nowhere else
78+ (re-exports from private modules) keep their full body here, via an
79+ explicit ``members:`` list.
80+ """
81+ prefix = f"{ package .path } ."
82+ exports = {str (export ): package .members [str (export )] for export in package .exports or ()}
83+ if not any (member .is_alias and not member .target_path .startswith (prefix ) for member in exports .values ()):
84+ return None
85+
86+ inline : list [str ] = []
87+ sections : dict [str , list [str ]] = {}
88+ for name in sorted (exports , key = str .lower ):
89+ member = exports [name ]
90+ # A target only gets an anchor on its canonical page if it is rendered
91+ # there, which the default `show_if_no_docstring: false` limits to
92+ # objects with docstrings.
93+ if member .is_alias and member .target_path .rpartition ("." )[0 ] in documented and member .has_docstrings :
94+ link_target = member .target_path
95+ else :
96+ inline .append (name )
97+ link_target = f"{ package .path } .{ name } "
98+ entry = f"- [`{ name } `][{ link_target } ]"
99+ target = member .final_target if member .is_alias else member
100+ if docstring := target .docstring :
101+ summary = docstring .value .split ("\n " , 1 )[0 ]
102+ entry += f" — { summary } "
103+ sections .setdefault (_KIND_SECTIONS [target .kind ], []).append (entry )
104+
105+ body = [f"::: { package .path } " , " options:" ]
106+ if inline :
107+ body += [" members:" , * (f" - { name } " for name in inline )]
108+ else :
109+ body += [" members: false" ]
110+ body .append ("" )
111+ for title in _KIND_SECTIONS .values ():
112+ if title in sections :
113+ body += [f"## { title } " , "" , * sections [title ], "" ]
114+ return "\n " .join (body )
115+
116+
48117def generate () -> list [NavItem ]:
49118 """Write ``docs/api/**.md`` stubs and return the API-section navigation."""
50119 if API_DIR .exists ():
51120 shutil .rmtree (API_DIR )
52121
53- src = ROOT / "src"
54122 root = _Node ()
123+ stubs : dict [Path , str ] = {}
124+ package_index : dict [str , Path ] = {}
125+ documented : set [str ] = set ()
55126
56- # `src/mcp-types` is a distribution directory, not an import package, so each
57- # package's dotted module path is taken relative to its own parent: deriving
58- # it from `src/` would emit the unimportable `mcp-types.mcp_types.*`.
59- for package in (src / "mcp" , src / "mcp-types" / "mcp_types" ):
127+ for package in PACKAGES :
60128 base = package .parent
61129 for path in sorted (package .rglob ("*.py" )):
62130 module_path = path .relative_to (base ).with_suffix ("" )
@@ -69,16 +137,31 @@ def generate() -> list[NavItem]:
69137 elif parts [- 1 ].startswith ("_" ):
70138 continue
71139
72- full_doc_path = API_DIR / doc_path
73- full_doc_path .parent .mkdir (parents = True , exist_ok = True )
74140 ident = "." .join (parts )
75- full_doc_path .write_text (f"::: { ident } \n " , encoding = "utf-8" )
141+ documented .add (ident )
142+ stubs [API_DIR / doc_path ] = f"::: { ident } \n "
143+ if len (parts ) == 1 :
144+ package_index [ident ] = API_DIR / doc_path
76145
77146 node = root
78147 for part in parts :
79148 node = node .child (part )
80149 node .url = f"api/{ doc_path .as_posix ()} "
81150
151+ # Load every package before inspecting any of them: aliases only resolve
152+ # once the module they point at is in the loader's collection.
153+ loader = griffe .GriffeLoader (search_paths = [str (package .parent ) for package in PACKAGES ])
154+ modules = {ident : loader .load (ident ) for ident in package_index }
155+ for ident , doc_path in package_index .items ():
156+ module = modules [ident ]
157+ assert isinstance (module , griffe .Module )
158+ if body := _compact_index (module , documented ):
159+ stubs [doc_path ] = body
160+
161+ for full_doc_path , stub in stubs .items ():
162+ full_doc_path .parent .mkdir (parents = True , exist_ok = True )
163+ full_doc_path .write_text (stub , encoding = "utf-8" )
164+
82165 return [root .children [name ].to_nav (name ) for name in sorted (root .children )]
83166
84167
0 commit comments