From ae85ba5af07211fda70f2aaa89d19530020f69bf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 12:39:52 -0500 Subject: [PATCH 1/9] docs(feat[api-style]): Enable API badge styling via sphinx-autodoc-api-style why: Add type and modifier badges to autodoc entries. what: - Add sphinx_autodoc_api_style to extra_extensions in docs/conf.py --- docs/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 32b63d0..ef7d05c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,7 +35,11 @@ source_branch="main", light_logo="img/libtmux.svg", dark_logo="img/libtmux.svg", - extra_extensions=["sphinx.ext.todo", "fastmcp_autodoc"], + extra_extensions=[ + "sphinx_autodoc_api_style", + "sphinx.ext.todo", + "fastmcp_autodoc", + ], intersphinx_mapping={ "python": ("https://docs.python.org/", None), "pytest": ("https://docs.pytest.org/en/stable/", None), From 85b8387766aa5e4df029c10d2b6f703d79d83630 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 12:39:52 -0500 Subject: [PATCH 2/9] py(deps[dev]): Add sphinx-autodoc-api-style, bump gp-sphinx packages to 0.0.1a5 why: Align docs dependencies with latest gp-sphinx workspace releases. what: - Pin sphinx-autodoc-api-style and bump gp-sphinx packages to 0.0.1a5 - Regenerate uv.lock --- pyproject.toml | 6 ++++-- uv.lock | 39 ++++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b37c9d7..a225e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,8 @@ libtmux-mcp = "libtmux_mcp:main" [dependency-groups] dev = [ # Docs - "gp-sphinx==0.0.1a1", + "gp-sphinx==0.0.1a5", + "sphinx-autodoc-api-style==0.0.1a5", "gp-libs", "sphinx-autobuild", # Testing @@ -77,7 +78,8 @@ dev = [ ] docs = [ - "gp-sphinx==0.0.1a1", + "gp-sphinx==0.0.1a5", + "sphinx-autodoc-api-style==0.0.1a5", "gp-libs", "sphinx-autobuild", ] diff --git a/uv.lock b/uv.lock index e9e86fc..892299d 100644 --- a/uv.lock +++ b/uv.lock @@ -713,7 +713,7 @@ wheels = [ [[package]] name = "gp-sphinx" -version = "0.0.1a1" +version = "0.0.1a5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, @@ -734,9 +734,9 @@ dependencies = [ { name = "sphinxext-opengraph" }, { name = "sphinxext-rediraffe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/89/aa7d03025bbcd036806a67299f04c1de302eda265b35046a1355240503da/gp_sphinx-0.0.1a1.tar.gz", hash = "sha256:70f99cdd2ef5f24aa160da4eb47f80933c8d69bce00383dc0eb60e8bd51663f5", size = 13991, upload-time = "2026-04-05T17:32:41.295Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/56/a3685ca51045e183caae9d0d996c3edf76653a3851e64ba91e1f2450ffa5/gp_sphinx-0.0.1a5.tar.gz", hash = "sha256:29998304bccc32d0f869109d1ee945263a528765a536665a5dc016fef30decee", size = 13992, upload-time = "2026-04-06T16:55:43.075Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/2a/21836581ec988b8c58cacac2bfb091bbb000b8fe682f62a2fa584674aa6b/gp_sphinx-0.0.1a1-py3-none-any.whl", hash = "sha256:6f0c73a1a13ba94bef7fb1c5368fe6e47dc4128ec948c27f08e834cdf41a2111", size = 14398, upload-time = "2026-04-05T17:32:31.292Z" }, + { url = "https://files.pythonhosted.org/packages/0c/65/917059394919f7909536fe5a881dd10c7e105a92cba0b5c0eb9c82d85459/gp_sphinx-0.0.1a5-py3-none-any.whl", hash = "sha256:15574494dd25049cf9d7ae47bd892db9cef53a08f5f3129a2cf90ddca4a32bd5", size = 14410, upload-time = "2026-04-06T16:55:32.866Z" }, ] [[package]] @@ -1073,6 +1073,7 @@ dev = [ { name = "ruff" }, { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-api-style" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] docs = [ @@ -1080,6 +1081,7 @@ docs = [ { name = "gp-sphinx" }, { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-api-style" }, ] lint = [ { name = "mypy" }, @@ -1111,7 +1113,7 @@ dev = [ { name = "codecov" }, { name = "coverage" }, { name = "gp-libs" }, - { name = "gp-sphinx", specifier = "==0.0.1a1" }, + { name = "gp-sphinx", specifier = "==0.0.1a5" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1121,12 +1123,14 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "sphinx-autobuild" }, + { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a5" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] docs = [ { name = "gp-libs" }, - { name = "gp-sphinx", specifier = "==0.0.1a1" }, + { name = "gp-sphinx", specifier = "==0.0.1a5" }, { name = "sphinx-autobuild" }, + { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a5" }, ] lint = [ { name = "mypy" }, @@ -2280,6 +2284,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, ] +[[package]] +name = "sphinx-autodoc-api-style" +version = "0.0.1a5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/b9/b980057e09b7b5f6502b78e10193113e594a20117b863edc3b030565f668/sphinx_autodoc_api_style-0.0.1a5.tar.gz", hash = "sha256:d775bcccb24bc7b886fd1a9f4f60be3ed1b7c857d486662c385b5cfd5ad63c4b", size = 11088, upload-time = "2026-04-06T16:55:45.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/04/53160196a2a8dee73669894d1c34aa478e81af0701640323341a1627c118/sphinx_autodoc_api_style-0.0.1a5-py3-none-any.whl", hash = "sha256:a78d0f83a5038c89daa0cf55c9175bb8aabb9eb47e68ca02a705f478afd30c3c", size = 11679, upload-time = "2026-04-06T16:55:35.855Z" }, +] + [[package]] name = "sphinx-autodoc-typehints" version = "3.0.1" @@ -2370,27 +2387,27 @@ wheels = [ [[package]] name = "sphinx-fonts" -version = "0.0.1a1" +version = "0.0.1a5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/17/c7bdfd74248812b5d7df452d65474817ba96d41ebd67862022938c914465/sphinx_fonts-0.0.1a1.tar.gz", hash = "sha256:2c4ae152636649d88151a1421293b7b147bab36d97ef7aa3e85ce52ce7984dad", size = 5628, upload-time = "2026-04-05T17:32:46.905Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/bd/c419420467fe1b249a8261f5253dfe5e17cf3a315cf98f5ce2bd32b85be2/sphinx_fonts-0.0.1a5.tar.gz", hash = "sha256:3e031378a973a6682e866b0260a8ce937276de571f69135bcbcaedfa905da395", size = 5624, upload-time = "2026-04-06T16:55:48.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/dd/595ac1e9f72c7bc9b19bc9cc2e5c3d429c4d20b9a344674d23b75269906f/sphinx_fonts-0.0.1a1-py3-none-any.whl", hash = "sha256:6b45590254b912fb1b19e08c1ab6c3ce42eb1e1d07333183005d1fd54bb92b6f", size = 4348, upload-time = "2026-04-05T17:32:38.579Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6c/6619babd3902262b7159d61f64a183194ee5670c45ab8353b5713e8a5856/sphinx_fonts-0.0.1a5-py3-none-any.whl", hash = "sha256:e8ce3cc7691fcab19cf44c069af4b24b37c0501add6cb8b60f9bbc858f0fb873", size = 4349, upload-time = "2026-04-06T16:55:40.747Z" }, ] [[package]] name = "sphinx-gptheme" -version = "0.0.1a1" +version = "0.0.1a5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "furo" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/8d/2bbde808fcc5aadb2e9cdb4c5ae0713ad88f3f57bfbdcfc6f0a4eae82bb2/sphinx_gptheme-0.0.1a1.tar.gz", hash = "sha256:d4b64b6dd6f8c213300820e1300ba075c56428946f4a903d1258440c0a9094d5", size = 14566, upload-time = "2026-04-05T17:32:47.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/fb/276020fdaae69e0fd2ac326888e0a478cc594678ff434f5c121457a08dcf/sphinx_gptheme-0.0.1a5.tar.gz", hash = "sha256:ba0303604641efa6a7ebf1f29c6d016abc4d29cf12770223fbc56fccbcba407e", size = 14569, upload-time = "2026-04-06T16:55:49.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/18/85b1d4550501d7f4a91d75a2ad39e6883e988e4217272e216e5a86b80a49/sphinx_gptheme-0.0.1a1-py3-none-any.whl", hash = "sha256:52a752136bda4641d001d8f32f59f3b492a631fe19cec116ba14c316351ba00d", size = 15624, upload-time = "2026-04-05T17:32:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/d17927243ee476e7909d93d966bb03c8eef5454fc40ac41401e36a7e0dfd/sphinx_gptheme-0.0.1a5-py3-none-any.whl", hash = "sha256:39771734aefe093d3c80060095921f4ff1e276123f5b5f92dc4327996d0e5a94", size = 15628, upload-time = "2026-04-06T16:55:41.81Z" }, ] [[package]] From e420efe01235c9665650ffe28a46bdd41ba41c41 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 14:19:08 -0500 Subject: [PATCH 3/9] py(deps[docs]): Add sphinx-autodoc-fastmcp and local gp-sphinx uv.sources why: Consume the new extension from gp-sphinx; optional path overrides for a sibling gp-sphinx clone during development. what: - dev/docs dependency on sphinx-autodoc-fastmcp - tool.uv.sources for gp-sphinx, sphinx-autodoc-api-style, sphinx-autodoc-fastmcp - mypy override for sphinx_autodoc_fastmcp - uv.lock --- pyproject.toml | 10 ++++++- uv.lock | 72 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a225e19..469186d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,11 +53,18 @@ Changes = "https://github.com/tmux-python/libtmux-mcp/blob/master/CHANGES" [project.scripts] libtmux-mcp = "libtmux_mcp:main" +# Sibling clone: ../gp-sphinx (git-pull/gp-sphinx). Override PyPI pins for local dev. +[tool.uv.sources] +gp-sphinx = { path = "../gp-sphinx/packages/gp-sphinx", editable = true } +sphinx-autodoc-api-style = { path = "../gp-sphinx/packages/sphinx-autodoc-api-style", editable = true } +sphinx-autodoc-fastmcp = { path = "../gp-sphinx/packages/sphinx-autodoc-fastmcp", editable = true } + [dependency-groups] dev = [ # Docs "gp-sphinx==0.0.1a5", "sphinx-autodoc-api-style==0.0.1a5", + "sphinx-autodoc-fastmcp==0.0.1a5", "gp-libs", "sphinx-autobuild", # Testing @@ -80,6 +87,7 @@ dev = [ docs = [ "gp-sphinx==0.0.1a5", "sphinx-autodoc-api-style==0.0.1a5", + "sphinx-autodoc-fastmcp==0.0.1a5", "gp-libs", "sphinx-autobuild", ] @@ -115,7 +123,7 @@ files = [ ] [[tool.mypy.overrides]] -module = ["fastmcp_autodoc", "docutils", "docutils.*"] +module = ["sphinx_autodoc_fastmcp", "sphinx_autodoc_fastmcp.*", "docutils", "docutils.*"] ignore_missing_imports = true [[tool.mypy.overrides]] diff --git a/uv.lock b/uv.lock index 892299d..632f9e1 100644 --- a/uv.lock +++ b/uv.lock @@ -714,7 +714,7 @@ wheels = [ [[package]] name = "gp-sphinx" version = "0.0.1a5" -source = { registry = "https://pypi.org/simple" } +source = { editable = "../gp-sphinx/packages/gp-sphinx" } dependencies = [ { name = "docutils" }, { name = "gp-libs" }, @@ -734,10 +734,25 @@ dependencies = [ { name = "sphinxext-opengraph" }, { name = "sphinxext-rediraffe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/56/a3685ca51045e183caae9d0d996c3edf76653a3851e64ba91e1f2450ffa5/gp_sphinx-0.0.1a5.tar.gz", hash = "sha256:29998304bccc32d0f869109d1ee945263a528765a536665a5dc016fef30decee", size = 13992, upload-time = "2026-04-06T16:55:43.075Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/65/917059394919f7909536fe5a881dd10c7e105a92cba0b5c0eb9c82d85459/gp_sphinx-0.0.1a5-py3-none-any.whl", hash = "sha256:15574494dd25049cf9d7ae47bd892db9cef53a08f5f3129a2cf90ddca4a32bd5", size = 14410, upload-time = "2026-04-06T16:55:32.866Z" }, + +[package.metadata] +requires-dist = [ + { name = "docutils" }, + { name = "gp-libs" }, + { name = "linkify-it-py" }, + { name = "myst-parser" }, + { name = "sphinx", specifier = "<9" }, + { name = "sphinx-argparse-neo", marker = "extra == 'argparse'", editable = "../gp-sphinx/packages/sphinx-argparse-neo" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-design" }, + { name = "sphinx-fonts", editable = "../gp-sphinx/packages/sphinx-fonts" }, + { name = "sphinx-gptheme", editable = "../gp-sphinx/packages/sphinx-gptheme" }, + { name = "sphinx-inline-tabs" }, + { name = "sphinxext-opengraph" }, + { name = "sphinxext-rediraffe" }, ] +provides-extras = ["argparse"] [[package]] name = "h11" @@ -1074,6 +1089,7 @@ dev = [ { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-autodoc-api-style" }, + { name = "sphinx-autodoc-fastmcp" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] docs = [ @@ -1082,6 +1098,7 @@ docs = [ { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-autodoc-api-style" }, + { name = "sphinx-autodoc-fastmcp" }, ] lint = [ { name = "mypy" }, @@ -1113,7 +1130,7 @@ dev = [ { name = "codecov" }, { name = "coverage" }, { name = "gp-libs" }, - { name = "gp-sphinx", specifier = "==0.0.1a5" }, + { name = "gp-sphinx", editable = "../gp-sphinx/packages/gp-sphinx" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1123,14 +1140,16 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "sphinx-autobuild" }, - { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a5" }, + { name = "sphinx-autodoc-api-style", editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" }, + { name = "sphinx-autodoc-fastmcp", editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] docs = [ { name = "gp-libs" }, - { name = "gp-sphinx", specifier = "==0.0.1a5" }, + { name = "gp-sphinx", editable = "../gp-sphinx/packages/gp-sphinx" }, { name = "sphinx-autobuild" }, - { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a5" }, + { name = "sphinx-autodoc-api-style", editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" }, + { name = "sphinx-autodoc-fastmcp", editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" }, ] lint = [ { name = "mypy" }, @@ -2287,16 +2306,27 @@ wheels = [ [[package]] name = "sphinx-autodoc-api-style" version = "0.0.1a5" -source = { registry = "https://pypi.org/simple" } +source = { editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/b9/b980057e09b7b5f6502b78e10193113e594a20117b863edc3b030565f668/sphinx_autodoc_api_style-0.0.1a5.tar.gz", hash = "sha256:d775bcccb24bc7b886fd1a9f4f60be3ed1b7c857d486662c385b5cfd5ad63c4b", size = 11088, upload-time = "2026-04-06T16:55:45.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/04/53160196a2a8dee73669894d1c34aa478e81af0701640323341a1627c118/sphinx_autodoc_api_style-0.0.1a5-py3-none-any.whl", hash = "sha256:a78d0f83a5038c89daa0cf55c9175bb8aabb9eb47e68ca02a705f478afd30c3c", size = 11679, upload-time = "2026-04-06T16:55:35.855Z" }, + +[package.metadata] +requires-dist = [{ name = "sphinx" }] + +[[package]] +name = "sphinx-autodoc-fastmcp" +version = "0.0.1a5" +source = { editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] +[package.metadata] +requires-dist = [{ name = "sphinx" }] + [[package]] name = "sphinx-autodoc-typehints" version = "3.0.1" @@ -2388,27 +2418,25 @@ wheels = [ [[package]] name = "sphinx-fonts" version = "0.0.1a5" -source = { registry = "https://pypi.org/simple" } +source = { editable = "../gp-sphinx/packages/sphinx-fonts" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/bd/c419420467fe1b249a8261f5253dfe5e17cf3a315cf98f5ce2bd32b85be2/sphinx_fonts-0.0.1a5.tar.gz", hash = "sha256:3e031378a973a6682e866b0260a8ce937276de571f69135bcbcaedfa905da395", size = 5624, upload-time = "2026-04-06T16:55:48.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/6c/6619babd3902262b7159d61f64a183194ee5670c45ab8353b5713e8a5856/sphinx_fonts-0.0.1a5-py3-none-any.whl", hash = "sha256:e8ce3cc7691fcab19cf44c069af4b24b37c0501add6cb8b60f9bbc858f0fb873", size = 4349, upload-time = "2026-04-06T16:55:40.747Z" }, -] + +[package.metadata] +requires-dist = [{ name = "sphinx" }] [[package]] name = "sphinx-gptheme" version = "0.0.1a5" -source = { registry = "https://pypi.org/simple" } +source = { editable = "../gp-sphinx/packages/sphinx-gptheme" } dependencies = [ { name = "furo" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/fb/276020fdaae69e0fd2ac326888e0a478cc594678ff434f5c121457a08dcf/sphinx_gptheme-0.0.1a5.tar.gz", hash = "sha256:ba0303604641efa6a7ebf1f29c6d016abc4d29cf12770223fbc56fccbcba407e", size = 14569, upload-time = "2026-04-06T16:55:49.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/bb/d17927243ee476e7909d93d966bb03c8eef5454fc40ac41401e36a7e0dfd/sphinx_gptheme-0.0.1a5-py3-none-any.whl", hash = "sha256:39771734aefe093d3c80060095921f4ff1e276123f5b5f92dc4327996d0e5a94", size = 15628, upload-time = "2026-04-06T16:55:41.81Z" }, -] + +[package.metadata] +requires-dist = [{ name = "furo" }] [[package]] name = "sphinx-inline-tabs" From e450aa440cbeb5b402ce6c8ac13e913885411a18 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 14:19:12 -0500 Subject: [PATCH 4/9] docs(conf): Use sphinx_autodoc_fastmcp and remove local _ext why: Drop the inlined fastmcp_autodoc extension in favor of the gp-sphinx package; configure tool modules, area map, and section badges. what: - extra_extensions and fastmcp_* settings in docs/conf.py - remove docs/_ext/fastmcp_autodoc.py; drop _ext from sys.path --- docs/_ext/fastmcp_autodoc.py | 1088 ---------------------------------- docs/conf.py | 39 +- 2 files changed, 37 insertions(+), 1090 deletions(-) delete mode 100644 docs/_ext/fastmcp_autodoc.py diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py deleted file mode 100644 index a21f185..0000000 --- a/docs/_ext/fastmcp_autodoc.py +++ /dev/null @@ -1,1088 +0,0 @@ -"""Sphinx extension for autodocumenting FastMCP tools. - -Builds documentation directly from docutils/Sphinx node API — no text -generation or markdown parsing. Tables, sections, and cross-references -are all proper doctree nodes. - -Provides two directives: - -- ``fastmcp-tool``: Autodocument a single MCP tool function. - Creates a section (visible in ToC) with safety badge, parameter table, - and return type. -- ``fastmcp-toolsummary``: Generate a summary table of all tools grouped - by safety tier. - -Usage in MyST:: - - ```{fastmcp-tool} server_tools.list_sessions - ``` - - ```{fastmcp-toolsummary} - ``` -""" - -from __future__ import annotations - -import importlib -import inspect -import logging -import re -import typing as t -from dataclasses import dataclass - -from docutils import nodes -from sphinx import addnodes -from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective - -if t.TYPE_CHECKING: - from sphinx.domains.std import StandardDomain - from sphinx.util.typing import ExtensionMetadata - -logger = logging.getLogger(__name__) - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- - -AREA_MAP: dict[str, str] = { - "server_tools": "sessions", - "session_tools": "sessions", - "window_tools": "windows", - "pane_tools": "panes", - "option_tools": "options", - "env_tools": "options", -} - -SECTION_BADGE_MAP: dict[str, str] = { - "Inspect": "readonly", - "Act": "mutating", - "Destroy": "destructive", -} - -TAG_READONLY = "readonly" -TAG_MUTATING = "mutating" -TAG_DESTRUCTIVE = "destructive" - -_MODEL_MODULE = "libtmux_mcp.models" -_MODEL_CLASSES: set[str] = { - "SessionInfo", - "WindowInfo", - "PaneInfo", - "PaneContentMatch", - "ServerInfo", - "OptionResult", - "OptionSetResult", - "EnvironmentResult", - "EnvironmentSetResult", - "WaitForTextResult", -} - - -# --------------------------------------------------------------------------- -# Data classes -# --------------------------------------------------------------------------- - - -@dataclass -class ParamInfo: - """Extracted parameter information.""" - - name: str - type_str: str - required: bool - default: str - description: str - - -@dataclass -class ToolInfo: - """Collected metadata for a single MCP tool.""" - - name: str - title: str - module_name: str - area: str - safety: str - annotations: dict[str, bool] - func: t.Callable[..., t.Any] - docstring: str - params: list[ParamInfo] - return_annotation: str - - -# --------------------------------------------------------------------------- -# Docstring + signature parsing -# --------------------------------------------------------------------------- - - -def _parse_numpy_params(docstring: str) -> dict[str, str]: - """Extract parameter descriptions from NumPy-style docstring.""" - params: dict[str, str] = {} - if not docstring: - return params - - lines = docstring.split("\n") - in_params = False - current_param: str | None = None - current_desc: list[str] = [] - - for line in lines: - stripped = line.strip() - indent = len(line) - len(line.lstrip()) - - if stripped == "Parameters": - in_params = True - continue - if in_params and stripped.startswith("---"): - continue - if in_params and stripped in ( - "Returns", - "Raises", - "Notes", - "Examples", - "See Also", - ): - if current_param: - params[current_param] = " ".join(current_desc).strip() - break - if in_params and not stripped: - continue - - if in_params: - param_match = re.match(r"^(\w+)\s*:", stripped) - if param_match and indent == 0: - if current_param: - params[current_param] = " ".join(current_desc).strip() - current_param = param_match.group(1) - current_desc = [] - elif current_param and indent > 0: - current_desc.append(stripped) - - if current_param: - params[current_param] = " ".join(current_desc).strip() - - return params - - -def _first_paragraph(docstring: str) -> str: - """Extract the first paragraph from a docstring.""" - if not docstring: - return "" - paragraphs = docstring.strip().split("\n\n") - return paragraphs[0].strip().replace("\n", " ") - - -def _format_annotation(ann: t.Any, *, strip_none: bool = False) -> str: - """Format a type annotation as a readable string. - - Parameters - ---------- - ann : Any - The annotation to format. - strip_none : bool - If True, remove ``| None`` from union types. Useful when the - parameter is already marked as optional. - """ - if ann is inspect.Parameter.empty: - return "" - if isinstance(ann, str): - result = ann - # Clean up t.Literal['a', 'b'] or Literal['a', 'b'] → 'a', 'b' - result = re.sub( - r"(?:t\.)?Literal\[([^\]]+)\]", - lambda m: m.group(1), - result, - ) - if strip_none: - result = re.sub(r"\s*\|\s*None\b", "", result).strip() - return result - if hasattr(ann, "__name__"): - return str(ann.__name__) - return str(ann).replace("typing.", "") - - -def _extract_params(func: t.Callable[..., t.Any]) -> list[ParamInfo]: - """Extract parameter info from function signature + docstring.""" - sig = inspect.signature(func) - doc_params = _parse_numpy_params(func.__doc__ or "") - params: list[ParamInfo] = [] - - for name, param in sig.parameters.items(): - is_optional = param.default != inspect.Parameter.empty - type_str = _format_annotation( - param.annotation, - strip_none=is_optional, - ) - - if is_optional: - if param.default is None: - default_str = "None" - elif isinstance(param.default, bool): - default_str = str(param.default) - elif isinstance(param.default, str): - default_str = repr(param.default) - else: - default_str = str(param.default) - required = False - else: - default_str = "" - required = True - - params.append( - ParamInfo( - name=name, - type_str=type_str, - required=required, - default=default_str, - description=doc_params.get(name, ""), - ) - ) - - return params - - -# --------------------------------------------------------------------------- -# Node construction helpers -# --------------------------------------------------------------------------- - - -def _make_table( - headers: list[str], - rows: list[list[str | nodes.Node]], - col_widths: list[int] | None = None, -) -> nodes.table: - """Build a docutils table node from headers and rows.""" - ncols = len(headers) - if col_widths is None: - col_widths = [100 // ncols] * ncols - - table = nodes.table("") - tgroup = nodes.tgroup("", cols=ncols) - table += tgroup - - for width in col_widths: - tgroup += nodes.colspec("", colwidth=width) - - # Header row - thead = nodes.thead("") - header_row = nodes.row("") - for header in headers: - entry = nodes.entry("") - entry += nodes.paragraph("", header) - header_row += entry - thead += header_row - tgroup += thead - - # Body rows - tbody = nodes.tbody("") - for row_data in rows: - row = nodes.row("") - for cell in row_data: - entry = nodes.entry("") - if isinstance(cell, nodes.Node): - entry += cell - else: - entry += nodes.paragraph("", str(cell)) - row += entry - tbody += row - tgroup += tbody - - return table - - -def _make_literal(text: str) -> nodes.literal: - """Create an inline code literal node.""" - return nodes.literal("", text) - - -def _single_type_xref(name: str) -> addnodes.pending_xref: - """Create a ``pending_xref`` for a single type name. - - Known model classes are qualified to ``libtmux_mcp.models.X``. - Builtins (``str``, ``list``, ``int``, etc.) target the Python domain. - """ - target = f"{_MODEL_MODULE}.{name}" if name in _MODEL_CLASSES else name - return addnodes.pending_xref( - "", - nodes.literal("", name), - refdomain="py", - reftype="class", - reftarget=target, - ) - - -def _make_type_xref(type_str: str) -> nodes.paragraph: - """Render a return type annotation with cross-reference links. - - Handles ``list[X]`` generics and bare type names. - Each type component becomes a ``pending_xref`` that Sphinx resolves - into a hyperlink (internal or intersphinx). - """ - para = nodes.paragraph("") - m = re.match(r"^(list|set|tuple)\[(.+)\]$", type_str) - if m: - container, inner = m.group(1), m.group(2) - para += _single_type_xref(container) - para += nodes.Text("[") - para += _single_type_xref(inner) - para += nodes.Text("]") - else: - para += _single_type_xref(type_str) - return para - - -def _make_para(*children: nodes.Node | str) -> nodes.paragraph: - """Create a paragraph from mixed text and node children.""" - para = nodes.paragraph("") - for child in children: - if isinstance(child, str): - para += nodes.Text(child) - else: - para += child - return para - - -def _parse_rst_inline( - text: str, - state: t.Any, - lineno: int, -) -> nodes.paragraph: - """Parse a string containing RST inline markup into a paragraph node. - - Handles ``code``, *emphasis*, **strong**, :role:`ref`, etc. - """ - parsed_nodes, _messages = state.inline_text(text, lineno) - para = nodes.paragraph("") - para += parsed_nodes - return para - - -def _make_type_cell(type_str: str) -> nodes.paragraph: - """Render a type annotation as comma-separated code literals. - - ``dict[str, str] | str`` becomes ``dict[str, str]``, ``str`` - ``'server', 'session', 'window'`` becomes ``'server'``, ``'session'``, ... - — each part in its own element so they wrap cleanly. - """ - # Split on | for union types - parts = [p.strip() for p in type_str.split("|")] - - # Further split quoted literal values: 'a', 'b', 'c' - expanded: list[str] = [] - for part in parts: - if re.match(r"^'[^']*'(\s*,\s*'[^']*')+$", part): - # Multiple quoted values like 'server', 'session', 'window' - expanded.extend(v.strip() for v in part.split(",")) - else: - expanded.append(part) - - para = nodes.paragraph("") - for i, part in enumerate(expanded): - if i > 0: - para += nodes.Text(", ") - para += nodes.literal("", part) - return para - - -def _make_type_cell_smart( - type_str: str, -) -> tuple[nodes.paragraph | str, bool]: - """Render a type annotation, detecting enum-only types. - - Returns (node, is_enum). If the type is purely quoted literal - values, returns ``enum`` as the type and True so the caller - can append the values to the description column instead. - """ - if not type_str: - return ("", False) - - parts = [p.strip() for p in type_str.split("|")] - - # Check if ALL parts are quoted strings (Literal enum values) - all_quoted = all(re.match(r"^'[^']*'$", p) for p in parts) - # Also handle comma-separated quoted values from Literal cleanup - if not all_quoted and len(parts) == 1: - sub = [s.strip() for s in parts[0].split(",")] - all_quoted = len(sub) > 1 and all(re.match(r"^'[^']*'$", s) for s in sub) - - if all_quoted: - return (_make_para(_make_literal("enum")), True) - - return (_make_type_cell(type_str), False) - - -def _extract_enum_values(type_str: str) -> list[str]: - """Extract individual enum values from a Literal type string.""" - parts = [p.strip() for p in type_str.split("|")] - values: list[str] = [] - for part in parts: - for sub in part.split(","): - sub = sub.strip() - if re.match(r"^'[^']*'$", sub): - values.append(sub) - return values - - -class _safety_badge_node(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] - """Custom node for safety badges with ARIA attributes in HTML output.""" - - -def _visit_safety_badge_html(self: t.Any, node: _safety_badge_node) -> None: - """Emit opening ```` with classes, role, and aria-label.""" - classes = " ".join(node.get("classes", [])) - safety = node.get("safety", "") - self.body.append( - f'' - ) - - -def _depart_safety_badge_html(self: t.Any, node: _safety_badge_node) -> None: - """Close the ````.""" - self.body.append("") - - -def _safety_badge(safety: str) -> _safety_badge_node: - """Create a colored safety badge node with ARIA attributes.""" - _base = ["sd-sphinx-override", "sd-badge"] - classes = { - "readonly": [*_base, "sd-bg-success", "sd-bg-text-success"], - "mutating": [*_base, "sd-bg-warning", "sd-bg-text-warning"], - "destructive": [*_base, "sd-bg-danger", "sd-bg-text-danger"], - } - badge = _safety_badge_node( - "", - nodes.Text(safety), - classes=classes.get(safety, []), - safety=safety, - ) - return badge - - -# --------------------------------------------------------------------------- -# Tool collection (runs at builder-inited) -# --------------------------------------------------------------------------- - - -class _ToolCollector: - """Mock FastMCP that captures tool registrations.""" - - def __init__(self) -> None: - self.tools: list[ToolInfo] = [] - self._current_module: str = "" - - def tool( - self, - title: str = "", - annotations: dict[str, bool] | None = None, - tags: set[str] | None = None, - ) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]: - annotations = annotations or {} - tags = tags or set() - - def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: - if TAG_DESTRUCTIVE in tags: - safety = "destructive" - elif TAG_MUTATING in tags: - safety = "mutating" - else: - safety = "readonly" - - module_name = self._current_module - area = AREA_MAP.get(module_name, module_name.replace("_tools", "")) - - self.tools.append( - ToolInfo( - name=func.__name__, - title=title or func.__name__.replace("_", " ").title(), - module_name=module_name, - area=area, - safety=safety, - annotations=annotations, - func=func, - docstring=func.__doc__ or "", - params=_extract_params(func), - return_annotation=_format_annotation( - inspect.signature(func).return_annotation, - ), - ) - ) - return func - - return decorator - - -def _collect_tools(app: Sphinx) -> None: - """Collect tool metadata from libtmux_mcp source at build time.""" - collector = _ToolCollector() - - tool_modules = [ - "server_tools", - "session_tools", - "window_tools", - "pane_tools", - "option_tools", - "env_tools", - ] - - for mod_name in tool_modules: - collector._current_module = mod_name - try: - mod = importlib.import_module(f"libtmux_mcp.tools.{mod_name}") - if hasattr(mod, "register"): - mod.register(collector) - except Exception: - logger.warning( - "fastmcp_autodoc: failed to load tool module %s", - mod_name, - exc_info=True, - ) - - app.env.fastmcp_tools = {tool.name: tool for tool in collector.tools} # type: ignore[attr-defined] - - -# --------------------------------------------------------------------------- -# Directives -# --------------------------------------------------------------------------- - - -class FastMCPToolDirective(SphinxDirective): - """Autodocument a single MCP tool as a proper section with table. - - Creates a section node (visible in ToC) containing: - - Safety badge + one-line description - - Parameter table (headers: Parameter, Type, Required, Default, Description) - - Return type - - Usage:: - - ```{fastmcp-tool} server_tools.list_sessions - ``` - """ - - required_arguments = 1 - optional_arguments = 0 - has_content = True - final_argument_whitespace = False - - def run(self) -> list[nodes.Node]: - """Build tool section header nodes.""" - arg = self.arguments[0] - func_name = arg.split(".")[-1] if "." in arg else arg - - tools: dict[str, ToolInfo] = getattr(self.env, "fastmcp_tools", {}) - tool = tools.get(func_name) - - if tool is None: - return [ - self.state.document.reporter.warning( - f"fastmcp-tool: tool '{func_name}' not found. " - f"Available: {', '.join(sorted(tools.keys()))}", - line=self.lineno, - ) - ] - - return self._build_tool_section(tool) - - def _build_tool_section(self, tool: ToolInfo) -> list[nodes.Node]: - """Build section header: title, badge, and description only. - - The parameter table is emitted separately by - ``FastMCPToolInputDirective`` so that hand-written judgment - content (Use when / Avoid when / examples) can appear between - the header and the table in the source file. - """ - document = self.state.document - - # Section with anchor ID - section_id = tool.name.replace("_", "-") - section = nodes.section() - section["ids"].append(section_id) - document.note_explicit_target(section) - - # Title: tool name + safety badge inline - title_node = nodes.title("", "") - title_node += nodes.literal("", tool.name) - title_node += nodes.Text(" ") - title_node += _safety_badge(tool.safety) - section += title_node - - # Description paragraph - first_para = _first_paragraph(tool.docstring) - desc_para = _parse_rst_inline(first_para, self.state, self.lineno) - section += desc_para - - # Returns (promoted — high-signal for tool selection) - if tool.return_annotation: - returns_para = nodes.paragraph("") - returns_para += nodes.strong("", "Returns: ") - type_para = _make_type_xref(tool.return_annotation) - for child in type_para.children: - returns_para += child.deepcopy() - section += returns_para - - return [section] - - -class FastMCPToolInputDirective(SphinxDirective): - """Emit the parameter table and return type for a tool. - - Place this AFTER hand-written judgment content so the table - appears at the end of the tool section. - - Usage:: - - ```{fastmcp-tool-input} server_tools.list_sessions - ``` - """ - - required_arguments = 1 - optional_arguments = 0 - has_content = False - - def run(self) -> list[nodes.Node]: - """Build parameter table and return type nodes.""" - arg = self.arguments[0] - func_name = arg.split(".")[-1] if "." in arg else arg - - tools: dict[str, ToolInfo] = getattr(self.env, "fastmcp_tools", {}) - tool = tools.get(func_name) - - if tool is None: - return [ - self.state.document.reporter.warning( - f"fastmcp-tool-input: tool '{func_name}' not found.", - line=self.lineno, - ) - ] - - result: list[nodes.Node] = [] - - # Parameter table - if tool.params: - result.append(_make_para(nodes.strong("", "Parameters"))) - headers = ["Parameter", "Type", "Required", "Default", "Description"] - rows: list[list[str | nodes.Node]] = [] - for p in tool.params: - # Build description node — parse RST inline markup - desc_node = self._build_description(p) - - # Type cell — detect enum-only types and simplify - type_cell, is_enum = _make_type_cell_smart(p.type_str) - - # If enum, append allowed values to description - if is_enum and p.type_str: - enum_values = _extract_enum_values(p.type_str) - if enum_values: - desc_node += nodes.Text(" One of: ") - for i, val in enumerate(enum_values): - if i > 0: - desc_node += nodes.Text(", ") - desc_node += nodes.literal("", val) - desc_node += nodes.Text(".") - - # Default — suppress "None" as visual noise - default_cell: str | nodes.Node = "—" - if p.default and p.default != "None": - default_cell = _make_para(_make_literal(p.default)) - - rows.append( - [ - _make_para(_make_literal(p.name)), - type_cell, - "yes" if p.required else "no", - default_cell, - desc_node, - ] - ) - result.append( - _make_table(headers, rows, col_widths=[15, 15, 8, 10, 52]), - ) - - return result - - def _build_description(self, p: ParamInfo) -> nodes.paragraph: - """Build a description paragraph, parsing RST inline markup.""" - if p.description: - return _parse_rst_inline( - p.description, - self.state, - self.lineno, - ) - return nodes.paragraph("", "—") - - -class FastMCPToolSummaryDirective(SphinxDirective): - """Generate a summary table of all tools grouped by safety tier. - - Produces three tables (Inspect, Act, Destroy) with tool names - linked to their sections on area pages. - - Usage:: - - ```{fastmcp-toolsummary} - ``` - """ - - required_arguments = 0 - optional_arguments = 0 - has_content = False - - def run(self) -> list[nodes.Node]: - """Build grouped summary tables.""" - tools: dict[str, ToolInfo] = getattr(self.env, "fastmcp_tools", {}) - - if not tools: - return [ - self.state.document.reporter.warning( - "fastmcp-toolsummary: no tools found.", - line=self.lineno, - ) - ] - - groups: dict[str, list[ToolInfo]] = { - "readonly": [], - "mutating": [], - "destructive": [], - } - for tool in tools.values(): - groups.setdefault(tool.safety, []).append(tool) - - result_nodes: list[nodes.Node] = [] - - tier_order = [ - ("readonly", "Inspect", "Read tmux state without changing anything."), - ("mutating", "Act", "Create or modify tmux objects."), - ("destructive", "Destroy", "Tear down tmux objects. Not reversible."), - ] - - for safety, label, desc in tier_order: - tier_tools = groups.get(safety, []) - if not tier_tools: - continue - - # Section for this tier - section = nodes.section() - section["ids"].append(label.lower()) - self.state.document.note_explicit_target(section) - section += nodes.title("", label) - section += nodes.paragraph("", desc) - - # Summary table - headers = ["Tool", "Description"] - rows: list[list[str | nodes.Node]] = [] - for tool in sorted(tier_tools, key=lambda t: t.name): - first_line = _first_paragraph(tool.docstring) - # Link to the tool's section on its area page - ref = nodes.reference("", "", internal=True) - ref["refuri"] = f"{tool.area}/#{tool.name.replace('_', '-')}" - ref += nodes.literal("", tool.name) - rows.append( - [ - _make_para(ref), - _parse_rst_inline(first_line, self.state, self.lineno), - ] - ) - section += _make_table(headers, rows, col_widths=[30, 70]) - - result_nodes.append(section) - - return result_nodes - - -# --------------------------------------------------------------------------- -# Extension setup -# --------------------------------------------------------------------------- - - -def _register_tool_labels(app: Sphinx, doctree: nodes.document) -> None: - """Register tool sections with StandardDomain for site-wide {ref} links. - - ``note_explicit_target()`` only registers with docutils, not with Sphinx's - StandardDomain. This hook mirrors the pattern used by - ``sphinx.ext.autosectionlabel`` so that ``{ref}`list-sessions``` works - from any page. - - The primary label uses just the tool name (no safety badge) so that - ``{ref}`` renders a clean ``tool_name`` link. Use ``{tool}`` role - for a link that includes the colored safety badge. - """ - domain = t.cast("StandardDomain", app.env.get_domain("std")) - docname = app.env.docname - for section in doctree.findall(nodes.section): - if not section["ids"]: - continue - section_id = section["ids"][0] - if section.children and isinstance(section[0], nodes.title): - # Only register sections whose title starts with a literal - # (tool sections generated by fastmcp-tool have nodes.literal - # as the first title child). Non-tool sections (e.g. "Inspect", - # "Act") don't need site-wide labels. - title_node = section[0] - tool_name = "" - for child in title_node.children: - if isinstance(child, nodes.literal): - tool_name = child.astext() - break - if not tool_name: - continue - domain.anonlabels[section_id] = (docname, section_id) - domain.labels[section_id] = (docname, section_id, tool_name) - - -_SECTION_BADGE_PAGES: set[str] = {"tools/index", "index"} - - -def _add_section_badges( - app: Sphinx, - doctree: nodes.document, - fromdocname: str, -) -> None: - """Replace parenthesized tier names with colored badges in headings. - - Matches both bare headings (``Inspect``) and parenthesized variants - (``Inspect (readonly)``). The parenthesized text is stripped and - replaced with a badge node. - - Only applied to pages in ``_SECTION_BADGE_PAGES`` — individual tool - pages already have per-tool badges, making section-level badges - redundant. - - Runs at ``doctree-resolved`` — section IDs are already frozen, so - modifying the title doesn't affect anchors or cross-refs. - """ - if fromdocname not in _SECTION_BADGE_PAGES: - return - for section in doctree.findall(nodes.section): - if not section.children or not isinstance(section[0], nodes.title): - continue - title_text = section[0].astext().strip() - - # Try exact match first ("Inspect") - safety = SECTION_BADGE_MAP.get(title_text) - if safety is not None: - section[0] += nodes.Text(" ") - section[0] += _safety_badge(safety) - continue - - # Try parenthesized match ("Inspect (readonly)") - m = re.match(r"^(\w+)\s*\((\w+)\)$", title_text) - if m: - heading, tier = m.group(1), m.group(2) - if heading in SECTION_BADGE_MAP and tier == SECTION_BADGE_MAP[heading]: - # Replace title children: strip parenthesized text, add badge - title_node = section[0] - title_node.clear() - title_node += nodes.Text(heading + " ") - title_node += _safety_badge(tier) - - -class _tool_ref_placeholder(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] - """Placeholder node for ``{tool}`` and ``{toolref}`` roles. - - Resolved at ``doctree-resolved`` by ``_resolve_tool_refs``. - The ``show_badge`` attribute controls whether the safety badge is appended. - """ - - -def _resolve_tool_refs( - app: Sphinx, - doctree: nodes.document, - fromdocname: str, -) -> None: - """Resolve ``{tool}``, ``{toolref}``, and ``{toolicon*}`` placeholders. - - ``{tool}`` renders as ``code`` + safety badge (text + icon). - ``{toolref}`` renders as ``code`` only (no badge). - ``{toolicon}``/``{tooliconl}`` — icon-only badge left of code. - ``{tooliconr}`` — icon-only badge right of code. - ``{tooliconil}`` — icon-only badge inside code, left of text. - ``{tooliconir}`` — icon-only badge inside code, right of text. - - Runs at ``doctree-resolved`` — after all labels are registered and - standard ``{ref}`` resolution is done. - """ - domain = t.cast("StandardDomain", app.env.get_domain("std")) - builder = app.builder - tool_data: dict[str, ToolInfo] = getattr(app.env, "fastmcp_tools", {}) - - for node in list(doctree.findall(_tool_ref_placeholder)): - target = node.get("reftarget", "") - show_badge = node.get("show_badge", True) - icon_pos = node.get("icon_pos", "") - label_info = domain.labels.get(target) - if label_info is None: - node.replace_self(nodes.literal("", target.replace("-", "_"))) - continue - - todocname, labelid, _title = label_info - tool_name = target.replace("-", "_") - - newnode = nodes.reference("", "", internal=True) - try: - newnode["refuri"] = builder.get_relative_uri(fromdocname, todocname) - if labelid: - newnode["refuri"] += "#" + labelid - except Exception: - logger.warning( - "fastmcp_autodoc: failed to resolve URI for %s -> %s", - fromdocname, - todocname, - ) - newnode["refuri"] = "#" + labelid - newnode["classes"].append("reference") - newnode["classes"].append("internal") - - if icon_pos: - tool_info = tool_data.get(tool_name) - badge = None - if tool_info: - badge = _safety_badge(tool_info.safety) - badge["classes"].append("icon-only") - if icon_pos.startswith("inline"): - badge["classes"].append("icon-only-inline") - badge.children.clear() - badge += nodes.Text("") - - if icon_pos == "left": - if badge: - newnode += badge - newnode += nodes.literal("", tool_name) - elif icon_pos == "right": - newnode += nodes.literal("", tool_name) - if badge: - newnode += badge - elif icon_pos == "inline-left": - code_node = nodes.literal("", "") - if badge: - code_node += badge - code_node += nodes.Text(tool_name) - newnode += code_node - elif icon_pos == "inline-right": - code_node = nodes.literal("", "") - code_node += nodes.Text(tool_name) - if badge: - code_node += badge - newnode += code_node - else: - newnode += nodes.literal("", tool_name) - if show_badge: - tool_info = tool_data.get(tool_name) - if tool_info: - newnode += nodes.Text(" ") - newnode += _safety_badge(tool_info.safety) - - node.replace_self(newnode) - - -def _tool_role( - name: str, - rawtext: str, - text: str, - lineno: int, - inliner: object, - options: dict[str, object] | None = None, - content: list[str] | None = None, -) -> tuple[list[nodes.Node], list[nodes.system_message]]: - """Inline role ``:tool:`capture-pane``` → linked tool name + safety badge. - - Creates a placeholder node resolved later by ``_resolve_tool_refs``. - """ - target = text.strip().replace("_", "-") - node = _tool_ref_placeholder(rawtext, reftarget=target, show_badge=True) - return [node], [] - - -def _toolref_role( - name: str, - rawtext: str, - text: str, - lineno: int, - inliner: object, - options: dict[str, object] | None = None, - content: list[str] | None = None, -) -> tuple[list[nodes.Node], list[nodes.system_message]]: - """Inline role ``:toolref:`capture-pane``` → code-linked tool name, no badge. - - Like ``{tool}`` but without the safety badge. Use in dense contexts - (tables, inline prose) where badges would be too heavy. - """ - target = text.strip().replace("_", "-") - node = _tool_ref_placeholder(rawtext, reftarget=target, show_badge=False) - return [node], [] - - -def _make_toolicon_role( - icon_pos: str, -) -> t.Callable[..., tuple[list[nodes.Node], list[nodes.system_message]]]: - """Create an icon-only tool reference role for a given position.""" - - def role_fn( - name: str, - rawtext: str, - text: str, - lineno: int, - inliner: object, - options: dict[str, object] | None = None, - content: list[str] | None = None, - ) -> tuple[list[nodes.Node], list[nodes.system_message]]: - target = text.strip().replace("_", "-") - node = _tool_ref_placeholder( - rawtext, - reftarget=target, - show_badge=False, - icon_pos=icon_pos, - ) - return [node], [] - - return role_fn - - -# {toolicon} is a convenience alias for {tooliconl} (both render icon-left) -_toolicon_role = _make_toolicon_role("left") -_tooliconl_role = _make_toolicon_role("left") -_tooliconr_role = _make_toolicon_role("right") -_tooliconil_role = _make_toolicon_role("inline-left") -_tooliconir_role = _make_toolicon_role("inline-right") - - -def _badge_role( - name: str, - rawtext: str, - text: str, - lineno: int, - inliner: object, - options: dict[str, object] | None = None, - content: list[str] | None = None, -) -> tuple[list[nodes.Node], list[nodes.system_message]]: - """Inline role ``:badge:`readonly``` → colored safety badge span.""" - return [_safety_badge(text.strip())], [] - - -def setup(app: Sphinx) -> ExtensionMetadata: - """Register the fastmcp_autodoc extension.""" - app.add_node( - _safety_badge_node, - html=(_visit_safety_badge_html, _depart_safety_badge_html), - ) - app.connect("builder-inited", _collect_tools) - app.connect("doctree-read", _register_tool_labels) - app.connect("doctree-resolved", _add_section_badges) - app.connect("doctree-resolved", _resolve_tool_refs) - app.add_role("tool", _tool_role) - app.add_role("toolref", _toolref_role) - app.add_role("toolicon", _toolicon_role) - app.add_role("tooliconl", _tooliconl_role) - app.add_role("tooliconr", _tooliconr_role) - app.add_role("tooliconil", _tooliconil_role) - app.add_role("tooliconir", _tooliconir_role) - app.add_role("badge", _badge_role) - app.add_directive("fastmcp-tool", FastMCPToolDirective) - app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective) - app.add_directive("fastmcp-toolsummary", FastMCPToolSummaryDirective) - - return { - "version": "0.1.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/conf.py b/docs/conf.py index ef7d05c..4aecb20 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,6 @@ project_src = project_root / "src" sys.path.insert(0, str(project_src)) -sys.path.insert(0, str(cwd / "_ext")) # package data about: dict[str, str] = {} @@ -38,7 +37,7 @@ extra_extensions=[ "sphinx_autodoc_api_style", "sphinx.ext.todo", - "fastmcp_autodoc", + "sphinx_autodoc_fastmcp", ], intersphinx_mapping={ "python": ("https://docs.python.org/", None), @@ -66,6 +65,42 @@ conf["myst_enable_extensions"] = [*conf["myst_enable_extensions"], "attrs_inline"] +conf["fastmcp_tool_modules"] = [ + "libtmux_mcp.tools.server_tools", + "libtmux_mcp.tools.session_tools", + "libtmux_mcp.tools.window_tools", + "libtmux_mcp.tools.pane_tools", + "libtmux_mcp.tools.option_tools", + "libtmux_mcp.tools.env_tools", +] +conf["fastmcp_area_map"] = { + "server_tools": "sessions", + "session_tools": "sessions", + "window_tools": "windows", + "pane_tools": "panes", + "option_tools": "options", + "env_tools": "options", +} +conf["fastmcp_model_module"] = "libtmux_mcp.models" +conf["fastmcp_model_classes"] = ( + "SessionInfo", + "WindowInfo", + "PaneInfo", + "PaneContentMatch", + "ServerInfo", + "OptionResult", + "OptionSetResult", + "EnvironmentResult", + "EnvironmentSetResult", + "WaitForTextResult", +) +conf["fastmcp_section_badge_map"] = { + "Inspect": "readonly", + "Act": "mutating", + "Destroy": "destructive", +} +conf["fastmcp_section_badge_pages"] = ("tools/index", "index") + _gp_setup = conf.pop("setup") From e42b271fdd6bf1bf06329322985c34086107c398 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 17:20:10 -0500 Subject: [PATCH 5/9] chore(uv): Sync lockfile for sphinx-autodoc-badges graph why: Lockfile reflects transitive editable deps from gp-sphinx packages. what: - uv.lock updates for sphinx-autodoc-api-style and sphinx-autodoc-fastmcp requires-dist --- uv.lock | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 632f9e1..fa034b5 100644 --- a/uv.lock +++ b/uv.lock @@ -2307,6 +2307,22 @@ wheels = [ name = "sphinx-autodoc-api-style" version = "0.0.1a5" source = { editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-badges" }, +] + +[package.metadata] +requires-dist = [ + { name = "sphinx" }, + { name = "sphinx-autodoc-badges", editable = "../gp-sphinx/packages/sphinx-autodoc-badges" }, +] + +[[package]] +name = "sphinx-autodoc-badges" +version = "0.0.1a5" +source = { editable = "../gp-sphinx/packages/sphinx-autodoc-badges" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -2322,10 +2338,14 @@ source = { editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-badges" }, ] [package.metadata] -requires-dist = [{ name = "sphinx" }] +requires-dist = [ + { name = "sphinx" }, + { name = "sphinx-autodoc-badges", editable = "../gp-sphinx/packages/sphinx-autodoc-badges" }, +] [[package]] name = "sphinx-autodoc-typehints" From 80861bb8c50d1f3980092e8003a1e2aff82c13d6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 19:56:13 -0500 Subject: [PATCH 6/9] docs(conf): Rewrite Pydantic markdown cross-refs in autodoc docstrings why: Pydantic's BaseModel uses Markdown-style [text][ref] cross-refs in docstrings, but autodoc processes them as RST, yielding citation-ref rendering instead of hyperlinks. what: - autodoc-process-docstring hook rewrites [`Name`][pkg.Name] to :any: roles - pyproject.toml: add fastmcp_autodoc to mypy ignore_missing_imports --- docs/conf.py | 21 +++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4aecb20..f1c462c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,6 +3,7 @@ from __future__ import annotations import pathlib +import re import sys import typing as t @@ -103,10 +104,30 @@ _gp_setup = conf.pop("setup") +# Matches Pydantic-style markdown cross-refs in RST docstrings: +# [DisplayText][qualified.Name] → :any:`DisplayText ` +# [`DisplayText`][qualified.Name] → :any:`DisplayText ` +# Display text may be wrapped in backticks — strip them before forming the role. +_MD_XREF = re.compile(r"\[`?([^`\]]+)`?\]\[([a-zA-Z_][a-zA-Z0-9_.]*)\]") + + +def _convert_md_xrefs( + app: Sphinx, + what: str, + name: str, + obj: object, + options: object, + lines: list[str], +) -> None: + """Rewrite Pydantic markdown cross-refs to RST :any: roles.""" + for i, line in enumerate(lines): + lines[i] = _MD_XREF.sub(r":any:`\1 <\2>`", line) + def setup(app: Sphinx) -> None: """Configure Sphinx app hooks and register project-specific JS.""" _gp_setup(app) + app.connect("autodoc-process-docstring", _convert_md_xrefs) app.add_js_file("js/prompt-copy.js", loading_method="defer") diff --git a/pyproject.toml b/pyproject.toml index 469186d..76d27bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ files = [ ] [[tool.mypy.overrides]] -module = ["sphinx_autodoc_fastmcp", "sphinx_autodoc_fastmcp.*", "docutils", "docutils.*"] +module = ["fastmcp_autodoc", "sphinx_autodoc_fastmcp", "sphinx_autodoc_fastmcp.*", "docutils", "docutils.*"] ignore_missing_imports = true [[tool.mypy.overrides]] From 167f06dc29c6b539d2afa4e51cb4cc45f44684ed Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Mon, 6 Apr 2026 20:51:29 -0500 Subject: [PATCH 7/9] py(deps): gp-sphinx 0.0.1a5 -> 0.0.1a6 why: Align dev docs dependencies with gp-sphinx 0.0.1a6 on PyPI. what: - Bump gp-sphinx ecosystem pins in pyproject.toml - Regenerate uv.lock --- pyproject.toml | 12 ++++++------ uv.lock | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 76d27bd..9edb7b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,9 +62,9 @@ sphinx-autodoc-fastmcp = { path = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" [dependency-groups] dev = [ # Docs - "gp-sphinx==0.0.1a5", - "sphinx-autodoc-api-style==0.0.1a5", - "sphinx-autodoc-fastmcp==0.0.1a5", + "gp-sphinx==0.0.1a6", + "sphinx-autodoc-api-style==0.0.1a6", + "sphinx-autodoc-fastmcp==0.0.1a6", "gp-libs", "sphinx-autobuild", # Testing @@ -85,9 +85,9 @@ dev = [ ] docs = [ - "gp-sphinx==0.0.1a5", - "sphinx-autodoc-api-style==0.0.1a5", - "sphinx-autodoc-fastmcp==0.0.1a5", + "gp-sphinx==0.0.1a6", + "sphinx-autodoc-api-style==0.0.1a6", + "sphinx-autodoc-fastmcp==0.0.1a6", "gp-libs", "sphinx-autobuild", ] diff --git a/uv.lock b/uv.lock index fa034b5..16b1c0b 100644 --- a/uv.lock +++ b/uv.lock @@ -713,7 +713,7 @@ wheels = [ [[package]] name = "gp-sphinx" -version = "0.0.1a5" +version = "0.0.1a6" source = { editable = "../gp-sphinx/packages/gp-sphinx" } dependencies = [ { name = "docutils" }, @@ -2305,7 +2305,7 @@ wheels = [ [[package]] name = "sphinx-autodoc-api-style" -version = "0.0.1a5" +version = "0.0.1a6" source = { editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -2321,7 +2321,7 @@ requires-dist = [ [[package]] name = "sphinx-autodoc-badges" -version = "0.0.1a5" +version = "0.0.1a6" source = { editable = "../gp-sphinx/packages/sphinx-autodoc-badges" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -2333,7 +2333,7 @@ requires-dist = [{ name = "sphinx" }] [[package]] name = "sphinx-autodoc-fastmcp" -version = "0.0.1a5" +version = "0.0.1a6" source = { editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -2437,7 +2437,7 @@ wheels = [ [[package]] name = "sphinx-fonts" -version = "0.0.1a5" +version = "0.0.1a6" source = { editable = "../gp-sphinx/packages/sphinx-fonts" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -2449,7 +2449,7 @@ requires-dist = [{ name = "sphinx" }] [[package]] name = "sphinx-gptheme" -version = "0.0.1a5" +version = "0.0.1a6" source = { editable = "../gp-sphinx/packages/sphinx-gptheme" } dependencies = [ { name = "furo" }, From ef18103388b91a068b0ba3a0ef2f0bf9ab6dbda1 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Apr 2026 08:21:33 -0500 Subject: [PATCH 8/9] py(deps): gp-sphinx 0.0.1a6 -> 0.0.1a7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Pick up the sphinx-argparse-neo multi-page duplicate-label scoping fix landed in gp-sphinx PR #16, and finish the migration away from the in-tree docs/_ext/fastmcp_autodoc extension that moved to the gp-sphinx sphinx-autodoc-fastmcp package. The [tool.uv.sources] block pinned gp-sphinx / sphinx-autodoc-api-style / sphinx-autodoc-fastmcp at ../gp-sphinx/packages/*, which only resolves in the local dev workspace — CI has been failing at the uv sync step continuously since the block was introduced. Alongside that block, docs(conf): Use sphinx_autodoc_fastmcp and remove local _ext deleted docs/_ext/fastmcp_autodoc.py but left the tests/docs/_ext harness pointing at it, so once CI could actually install deps it would fail again at pytest collection on ImportError: fastmcp_autodoc. what: - Drop the [tool.uv.sources] block so all three packages resolve from PyPI in every environment - Bump gp-sphinx and sibling package pins from 0.0.1a6 to 0.0.1a7 - Remove the orphaned tests/docs/_ext/ harness (test_fastmcp_autodoc, conftest that added the removed docs/_ext to sys.path, and the tests/docs package markers); equivalent coverage now lives in gp-sphinx's sphinx-autodoc-fastmcp test suite - Drop the dead fastmcp_autodoc and tests.docs.* entries from the mypy overrides table - Regenerate uv.lock --- pyproject.toml | 24 +- tests/docs/__init__.py | 1 - tests/docs/_ext/__init__.py | 1 - tests/docs/_ext/conftest.py | 10 - tests/docs/_ext/test_fastmcp_autodoc.py | 686 ------------------------ uv.lock | 94 ++-- 6 files changed, 46 insertions(+), 770 deletions(-) delete mode 100644 tests/docs/__init__.py delete mode 100644 tests/docs/_ext/__init__.py delete mode 100644 tests/docs/_ext/conftest.py delete mode 100644 tests/docs/_ext/test_fastmcp_autodoc.py diff --git a/pyproject.toml b/pyproject.toml index 9edb7b8..cb09565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,18 +53,12 @@ Changes = "https://github.com/tmux-python/libtmux-mcp/blob/master/CHANGES" [project.scripts] libtmux-mcp = "libtmux_mcp:main" -# Sibling clone: ../gp-sphinx (git-pull/gp-sphinx). Override PyPI pins for local dev. -[tool.uv.sources] -gp-sphinx = { path = "../gp-sphinx/packages/gp-sphinx", editable = true } -sphinx-autodoc-api-style = { path = "../gp-sphinx/packages/sphinx-autodoc-api-style", editable = true } -sphinx-autodoc-fastmcp = { path = "../gp-sphinx/packages/sphinx-autodoc-fastmcp", editable = true } - [dependency-groups] dev = [ # Docs - "gp-sphinx==0.0.1a6", - "sphinx-autodoc-api-style==0.0.1a6", - "sphinx-autodoc-fastmcp==0.0.1a6", + "gp-sphinx==0.0.1a7", + "sphinx-autodoc-api-style==0.0.1a7", + "sphinx-autodoc-fastmcp==0.0.1a7", "gp-libs", "sphinx-autobuild", # Testing @@ -85,9 +79,9 @@ dev = [ ] docs = [ - "gp-sphinx==0.0.1a6", - "sphinx-autodoc-api-style==0.0.1a6", - "sphinx-autodoc-fastmcp==0.0.1a6", + "gp-sphinx==0.0.1a7", + "sphinx-autodoc-api-style==0.0.1a7", + "sphinx-autodoc-fastmcp==0.0.1a7", "gp-libs", "sphinx-autobuild", ] @@ -123,13 +117,9 @@ files = [ ] [[tool.mypy.overrides]] -module = ["fastmcp_autodoc", "sphinx_autodoc_fastmcp", "sphinx_autodoc_fastmcp.*", "docutils", "docutils.*"] +module = ["sphinx_autodoc_fastmcp", "sphinx_autodoc_fastmcp.*", "docutils", "docutils.*"] ignore_missing_imports = true -[[tool.mypy.overrides]] -module = ["tests.docs.*"] -disable_error_code = ["untyped-decorator"] - [tool.coverage.run] branch = true parallel = true diff --git a/tests/docs/__init__.py b/tests/docs/__init__.py deleted file mode 100644 index 5a82d0c..0000000 --- a/tests/docs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for documentation extensions.""" diff --git a/tests/docs/_ext/__init__.py b/tests/docs/_ext/__init__.py deleted file mode 100644 index 7856f50..0000000 --- a/tests/docs/_ext/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for custom Sphinx extensions.""" diff --git a/tests/docs/_ext/conftest.py b/tests/docs/_ext/conftest.py deleted file mode 100644 index e7547fa..0000000 --- a/tests/docs/_ext/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Fixtures and configuration for docs extension tests.""" - -from __future__ import annotations - -import pathlib -import sys - -docs_ext_path = pathlib.Path(__file__).parent.parent.parent.parent / "docs" / "_ext" -if str(docs_ext_path) not in sys.path: - sys.path.insert(0, str(docs_ext_path)) diff --git a/tests/docs/_ext/test_fastmcp_autodoc.py b/tests/docs/_ext/test_fastmcp_autodoc.py deleted file mode 100644 index d457572..0000000 --- a/tests/docs/_ext/test_fastmcp_autodoc.py +++ /dev/null @@ -1,686 +0,0 @@ -"""Tests for fastmcp_autodoc Sphinx extension.""" - -from __future__ import annotations - -import typing as t - -import fastmcp_autodoc -import pytest -from docutils import nodes - -# --------------------------------------------------------------------------- -# _parse_numpy_params -# --------------------------------------------------------------------------- - - -class ParseNumpyParamsFixture(t.NamedTuple): - """Test fixture for NumPy docstring parameter parsing.""" - - test_id: str - docstring: str - expected: dict[str, str] - - -PARSE_NUMPY_PARAMS_FIXTURES: list[ParseNumpyParamsFixture] = [ - ParseNumpyParamsFixture( - test_id="basic", - docstring=( - "Do something.\n\n" - "Parameters\n" - "----------\n" - "name : str\n" - " The name.\n" - "\n" - "Returns\n" - "-------\n" - "str\n" - ), - expected={"name": "The name."}, - ), - ParseNumpyParamsFixture( - test_id="multiple_params", - docstring=( - "Do something.\n\n" - "Parameters\n" - "----------\n" - "socket_name : str, optional\n" - " tmux socket name.\n" - "filters : dict or str, optional\n" - ' Django-style filters (e.g. ``{"key": "val"}``).\n' - "\n" - "Returns\n" - "-------\n" - ), - expected={ - "socket_name": "tmux socket name.", - "filters": 'Django-style filters (e.g. ``{"key": "val"}``).', - }, - ), - ParseNumpyParamsFixture( - test_id="multiline_description", - docstring=( - "Summary.\n\n" - "Parameters\n" - "----------\n" - "keys : str\n" - " The keys or text to send.\n" - " Can span multiple lines.\n" - "\n" - "Returns\n" - "-------\n" - ), - expected={"keys": "The keys or text to send. Can span multiple lines."}, - ), - ParseNumpyParamsFixture( - test_id="empty_docstring", - docstring="", - expected={}, - ), - ParseNumpyParamsFixture( - test_id="no_parameters_section", - docstring="Do something.\n\nReturns\n-------\nstr\n", - expected={}, - ), -] - - -@pytest.mark.parametrize( - PARSE_NUMPY_PARAMS_FIXTURES[0]._fields, - PARSE_NUMPY_PARAMS_FIXTURES, - ids=[f.test_id for f in PARSE_NUMPY_PARAMS_FIXTURES], -) -def test_parse_numpy_params( - test_id: str, - docstring: str, - expected: dict[str, str], -) -> None: - """_parse_numpy_params extracts parameter descriptions.""" - result = fastmcp_autodoc._parse_numpy_params(docstring) - assert result == expected - - -# --------------------------------------------------------------------------- -# _first_paragraph -# --------------------------------------------------------------------------- - - -class FirstParagraphFixture(t.NamedTuple): - """Test fixture for first paragraph extraction.""" - - test_id: str - docstring: str - expected: str - - -FIRST_PARAGRAPH_FIXTURES: list[FirstParagraphFixture] = [ - FirstParagraphFixture( - test_id="simple", - docstring="List all tmux sessions.", - expected="List all tmux sessions.", - ), - FirstParagraphFixture( - test_id="multiline_first_para", - docstring="Capture the visible contents\nof a tmux pane.\n\nMore detail.", - expected="Capture the visible contents of a tmux pane.", - ), - FirstParagraphFixture( - test_id="empty", - docstring="", - expected="", - ), -] - - -@pytest.mark.parametrize( - FIRST_PARAGRAPH_FIXTURES[0]._fields, - FIRST_PARAGRAPH_FIXTURES, - ids=[f.test_id for f in FIRST_PARAGRAPH_FIXTURES], -) -def test_first_paragraph( - test_id: str, - docstring: str, - expected: str, -) -> None: - """_first_paragraph extracts the first paragraph.""" - result = fastmcp_autodoc._first_paragraph(docstring) - assert result == expected - - -# --------------------------------------------------------------------------- -# _format_annotation -# --------------------------------------------------------------------------- - - -class FormatAnnotationFixture(t.NamedTuple): - """Test fixture for annotation formatting.""" - - test_id: str - annotation: t.Any - strip_none: bool - expected: str - - -FORMAT_ANNOTATION_FIXTURES: list[FormatAnnotationFixture] = [ - FormatAnnotationFixture( - test_id="string_with_none", - annotation="str | None", - strip_none=False, - expected="str | None", - ), - FormatAnnotationFixture( - test_id="string_strip_none", - annotation="str | None", - strip_none=True, - expected="str", - ), - FormatAnnotationFixture( - test_id="complex_strip_none", - annotation="dict[str, str] | str | None", - strip_none=True, - expected="dict[str, str] | str", - ), - FormatAnnotationFixture( - test_id="no_none_strip_noop", - annotation="str", - strip_none=True, - expected="str", - ), - FormatAnnotationFixture( - test_id="literal_cleanup", - annotation="t.Literal['server', 'session', 'window', 'pane'] | None", - strip_none=True, - expected="'server', 'session', 'window', 'pane'", - ), - FormatAnnotationFixture( - test_id="literal_cleanup_no_strip", - annotation="t.Literal['server', 'session', 'window', 'pane']", - strip_none=False, - expected="'server', 'session', 'window', 'pane'", - ), - FormatAnnotationFixture( - test_id="literal_no_prefix", - annotation="Literal['before', 'after']", - strip_none=False, - expected="'before', 'after'", - ), - FormatAnnotationFixture( - test_id="int_type", - annotation=int, - strip_none=False, - expected="int", - ), - FormatAnnotationFixture( - test_id="empty", - annotation="", - strip_none=False, - expected="", - ), -] - - -@pytest.mark.parametrize( - FORMAT_ANNOTATION_FIXTURES[0]._fields, - FORMAT_ANNOTATION_FIXTURES, - ids=[f.test_id for f in FORMAT_ANNOTATION_FIXTURES], -) -def test_format_annotation( - test_id: str, - annotation: t.Any, - strip_none: bool, - expected: str, -) -> None: - """_format_annotation formats type annotations correctly.""" - import inspect - - if annotation == "": - annotation = inspect.Parameter.empty - expected = "" - - result = fastmcp_autodoc._format_annotation(annotation, strip_none=strip_none) - assert result == expected - - -# --------------------------------------------------------------------------- -# _ToolCollector -# --------------------------------------------------------------------------- - - -def test_tool_collector_captures_registrations() -> None: - """_ToolCollector captures tool metadata from register() calls.""" - collector = fastmcp_autodoc._ToolCollector() - collector._current_module = "server_tools" - - @collector.tool( - title="List Sessions", - annotations={"readOnlyHint": True}, - tags={"readonly"}, - ) - def list_sessions(socket_name: str | None = None) -> list[str]: - """List all tmux sessions. - - Parameters - ---------- - socket_name : str, optional - tmux socket name. - - Returns - ------- - list[str] - """ - return [] - - assert len(collector.tools) == 1 - tool = collector.tools[0] - assert tool.name == "list_sessions" - assert tool.title == "List Sessions" - assert tool.safety == "readonly" - assert tool.area == "sessions" - assert tool.module_name == "server_tools" - assert len(tool.params) == 1 - assert tool.params[0].name == "socket_name" - assert tool.params[0].required is False - assert tool.params[0].description == "tmux socket name." - - -def test_tool_collector_safety_tiers() -> None: - """_ToolCollector correctly determines safety tier from tags.""" - collector = fastmcp_autodoc._ToolCollector() - collector._current_module = "test_tools" - - @collector.tool(tags={"readonly"}) - def read_tool() -> str: - """Read.""" - return "" - - @collector.tool(tags={"mutating"}) - def write_tool() -> str: - """Write.""" - return "" - - @collector.tool(tags={"destructive"}) - def destroy_tool() -> str: - """Destroy.""" - return "" - - assert collector.tools[0].safety == "readonly" - assert collector.tools[1].safety == "mutating" - assert collector.tools[2].safety == "destructive" - - -def test_tool_collector_strips_none_for_optional_params() -> None: - """Optional parameters should have | None stripped from type.""" - collector = fastmcp_autodoc._ToolCollector() - collector._current_module = "test_tools" - - @collector.tool(tags={"readonly"}) - def my_tool( - required_param: str, - optional_param: str | None = None, - ) -> str: - """Test. - - Parameters - ---------- - required_param : str - Required. - optional_param : str, optional - Optional. - - Returns - ------- - str - """ - return "" - - tool = collector.tools[0] - required = next(p for p in tool.params if p.name == "required_param") - optional = next(p for p in tool.params if p.name == "optional_param") - - assert required.required is True - assert "None" not in required.type_str or required.type_str == "str" - - assert optional.required is False - # | None should be stripped for optional params - assert optional.type_str == "str" - - -# --------------------------------------------------------------------------- -# _make_table -# --------------------------------------------------------------------------- - - -def test_make_table_structure() -> None: - """_make_table creates proper docutils table node hierarchy.""" - from docutils import nodes - - table = fastmcp_autodoc._make_table( - headers=["Name", "Type"], - rows=[["foo", "str"], ["bar", "int"]], - ) - - assert isinstance(table, nodes.table) - tgroup = table[0] - assert isinstance(tgroup, nodes.tgroup) - assert tgroup["cols"] == 2 - - # Header - thead = tgroup.children[2] # after 2 colspecs - assert isinstance(thead, nodes.thead) - header_row = thead[0] - assert len(header_row) == 2 - - # Body - tbody = tgroup.children[3] - assert isinstance(tbody, nodes.tbody) - assert len(tbody) == 2 # 2 data rows - - -def test_make_table_with_node_cells() -> None: - """_make_table handles Node objects as cell values.""" - from docutils import nodes - - literal = nodes.literal("", "code") - para = nodes.paragraph("", "") - para += literal - - table = fastmcp_autodoc._make_table( - headers=["Col"], - rows=[[para]], - ) - - # Just check the table built without error - assert isinstance(table, nodes.table) - - -# --------------------------------------------------------------------------- -# _safety_badge -# --------------------------------------------------------------------------- - - -def test_make_type_cell_splits_union() -> None: - """_make_type_cell splits union types into comma-separated literals.""" - from docutils import nodes - - para = fastmcp_autodoc._make_type_cell("dict[str, str] | str") - literals = [c for c in para.children if isinstance(c, nodes.literal)] - texts = [c.astext() for c in literals] - assert texts == ["dict[str, str]", "str"] - - # Separators should be Text nodes with ", " - text_nodes = [c for c in para.children if isinstance(c, nodes.Text)] - assert any(", " in c.astext() for c in text_nodes) - - -def test_make_type_cell_splits_literal_values() -> None: - """_make_type_cell splits quoted literal values into separate literals.""" - from docutils import nodes - - para = fastmcp_autodoc._make_type_cell("'server', 'session', 'window'") - literals = [c for c in para.children if isinstance(c, nodes.literal)] - texts = [c.astext() for c in literals] - assert texts == ["'server'", "'session'", "'window'"] - - -def test_make_type_cell_single_type() -> None: - """_make_type_cell handles single types without splitting.""" - from docutils import nodes - - para = fastmcp_autodoc._make_type_cell("str") - literals = [c for c in para.children if isinstance(c, nodes.literal)] - assert len(literals) == 1 - assert literals[0].astext() == "str" - - -def test_safety_badge_classes() -> None: - """_safety_badge creates badge nodes with correct CSS classes.""" - badge = fastmcp_autodoc._safety_badge("readonly") - assert "sd-bg-success" in badge["classes"] - - badge = fastmcp_autodoc._safety_badge("mutating") - assert "sd-bg-warning" in badge["classes"] - - badge = fastmcp_autodoc._safety_badge("destructive") - assert "sd-bg-danger" in badge["classes"] - - -def test_safety_badge_aria_attributes() -> None: - """_safety_badge stores safety tier for ARIA output.""" - badge = fastmcp_autodoc._safety_badge("readonly") - assert badge["safety"] == "readonly" - assert badge.astext() == "readonly" - - badge = fastmcp_autodoc._safety_badge("destructive") - assert badge["safety"] == "destructive" - - -# --------------------------------------------------------------------------- -# _make_type_xref -# --------------------------------------------------------------------------- - - -def test_make_type_xref_model_class() -> None: - """_make_type_xref creates pending_xref for known model classes.""" - from sphinx import addnodes - - para = fastmcp_autodoc._make_type_xref("PaneInfo") - assert len(para.children) == 1 - xref = para.children[0] - assert isinstance(xref, addnodes.pending_xref) - assert xref["refdomain"] == "py" - assert xref["reftype"] == "class" - assert xref["reftarget"] == "libtmux_mcp.models.PaneInfo" - assert xref.children[0].astext() == "PaneInfo" - - -def test_make_type_xref_list_of_model() -> None: - """_make_type_xref handles list[SessionInfo] with xrefs for both.""" - from sphinx import addnodes - - para = fastmcp_autodoc._make_type_xref("list[SessionInfo]") - # list xref, "[", SessionInfo xref, "]" - assert len(para.children) == 4 - list_xref = para.children[0] - assert isinstance(list_xref, addnodes.pending_xref) - assert list_xref["reftarget"] == "list" - - inner_xref = para.children[2] - assert isinstance(inner_xref, addnodes.pending_xref) - assert inner_xref["reftarget"] == "libtmux_mcp.models.SessionInfo" - - -def test_make_type_xref_builtin() -> None: - """_make_type_xref creates pending_xref for builtins like str.""" - from sphinx import addnodes - - para = fastmcp_autodoc._make_type_xref("str") - xref = para.children[0] - assert isinstance(xref, addnodes.pending_xref) - assert xref["reftarget"] == "str" - assert xref["refdomain"] == "py" - - -def test_make_type_xref_unknown() -> None: - """_make_type_xref still creates pending_xref for unknown types.""" - from sphinx import addnodes - - para = fastmcp_autodoc._make_type_xref("SomeUnknown") - xref = para.children[0] - assert isinstance(xref, addnodes.pending_xref) - assert xref["reftarget"] == "SomeUnknown" - - -# --------------------------------------------------------------------------- -# SECTION_BADGE_MAP + _add_section_badges -# --------------------------------------------------------------------------- - - -def test_section_badge_map_headings() -> None: - """SECTION_BADGE_MAP maps group headings to safety tiers.""" - m = fastmcp_autodoc.SECTION_BADGE_MAP - assert m["Inspect"] == "readonly" - assert m["Act"] == "mutating" - assert m["Destroy"] == "destructive" - - -def _make_doc_with_section( - section_id: str, title_text: str -) -> tuple[nodes.document, nodes.section, nodes.title]: - """Build a minimal doctree with one section.""" - from docutils import nodes - from docutils.frontend import OptionParser - from docutils.utils import new_document - - settings = OptionParser(components=()).get_default_values() - doc = new_document("test", settings) - section = nodes.section(ids=[section_id]) - title = nodes.title("", title_text) - section += title - doc += section - return doc, section, title - - -def test_add_section_badges_appends_badge_on_tools_index() -> None: - """_add_section_badges appends badge when fromdocname is tools/index.""" - doc, _section, title = _make_doc_with_section("inspect", "Inspect") - - fastmcp_autodoc._add_section_badges(None, doc, "tools/index") - - assert len(title.children) == 3 - badge = title.children[2] - assert isinstance(badge, fastmcp_autodoc._safety_badge_node) - assert "sd-bg-success" in badge["classes"] - assert badge.astext() == "readonly" - - -def test_add_section_badges_preserves_section_id() -> None: - """_add_section_badges does not change the section ID.""" - doc, section, _title = _make_doc_with_section("inspect", "Inspect") - - fastmcp_autodoc._add_section_badges(None, doc, "tools/index") - - assert section["ids"] == ["inspect"] - - -def test_add_section_badges_skips_non_index_pages() -> None: - """_add_section_badges skips individual tool pages (redundant).""" - doc, _section, title = _make_doc_with_section("inspect", "Inspect") - - fastmcp_autodoc._add_section_badges(None, doc, "tools/sessions") - - assert len(title.children) == 1 - assert title.astext() == "Inspect" - - -def test_add_section_badges_ignores_non_matching() -> None: - """_add_section_badges leaves non-matching headings untouched.""" - doc, _section, title = _make_doc_with_section("overview", "Overview") - - fastmcp_autodoc._add_section_badges(None, doc, "tools/index") - - assert len(title.children) == 1 - assert title.astext() == "Overview" - - -def test_add_section_badges_parenthesized_on_index() -> None: - """_add_section_badges replaces 'Inspect (readonly)' with badge on index.""" - doc, _section, title = _make_doc_with_section( - "inspect-readonly", "Inspect (readonly)" - ) - - fastmcp_autodoc._add_section_badges(None, doc, "index") - - # Title should be: Text("Inspect "), inline(badge) - assert len(title.children) == 2 - assert title.children[0].astext() == "Inspect " - badge = title.children[1] - assert isinstance(badge, fastmcp_autodoc._safety_badge_node) - assert "sd-bg-success" in badge["classes"] - assert badge.astext() == "readonly" - - -def test_add_section_badges_works_on_homepage() -> None: - """_add_section_badges runs on the homepage (fromdocname='index').""" - doc, _section, title = _make_doc_with_section("act", "Act") - - fastmcp_autodoc._add_section_badges(None, doc, "index") - - assert len(title.children) == 3 - badge = title.children[2] - assert isinstance(badge, fastmcp_autodoc._safety_badge_node) - assert "sd-bg-warning" in badge["classes"] - - -# --------------------------------------------------------------------------- -# Integration: collect real tools -# --------------------------------------------------------------------------- - - -def test_collect_real_tools() -> None: - """Collecting tools from libtmux_mcp source produces expected results.""" - collector = fastmcp_autodoc._ToolCollector() - - tool_modules = [ - "server_tools", - "session_tools", - "window_tools", - "pane_tools", - "option_tools", - "env_tools", - ] - - import importlib - - for mod_name in tool_modules: - collector._current_module = mod_name - mod = importlib.import_module(f"libtmux_mcp.tools.{mod_name}") - mod.register(collector) - - tools = {t.name: t for t in collector.tools} - - # Should have all expected tools - assert "list_sessions" in tools - assert "capture_pane" in tools - assert "send_keys" in tools - assert "kill_server" in tools - assert "show_option" in tools - assert "show_environment" in tools - - # Safety tiers should be correct - assert tools["list_sessions"].safety == "readonly" - assert tools["send_keys"].safety == "mutating" - assert tools["kill_server"].safety == "destructive" - - # Parameters should be extracted - ls = tools["list_sessions"] - param_names = [p.name for p in ls.params] - assert "socket_name" in param_names - assert "filters" in param_names - - # Descriptions should be parsed from docstrings - socket_param = next(p for p in ls.params if p.name == "socket_name") - assert "socket" in socket_param.description.lower() - - # Optional params should have | None stripped - assert socket_param.type_str == "str" - assert socket_param.required is False - - -def test_collect_real_tools_total_count() -> None: - """All 27 tools should be collected.""" - collector = fastmcp_autodoc._ToolCollector() - - import importlib - - for mod_name in [ - "server_tools", - "session_tools", - "window_tools", - "pane_tools", - "option_tools", - "env_tools", - ]: - collector._current_module = mod_name - mod = importlib.import_module(f"libtmux_mcp.tools.{mod_name}") - mod.register(collector) - - assert len(collector.tools) == 27 diff --git a/uv.lock b/uv.lock index 16b1c0b..42aad60 100644 --- a/uv.lock +++ b/uv.lock @@ -713,8 +713,8 @@ wheels = [ [[package]] name = "gp-sphinx" -version = "0.0.1a6" -source = { editable = "../gp-sphinx/packages/gp-sphinx" } +version = "0.0.1a7" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "gp-libs" }, @@ -734,25 +734,10 @@ dependencies = [ { name = "sphinxext-opengraph" }, { name = "sphinxext-rediraffe" }, ] - -[package.metadata] -requires-dist = [ - { name = "docutils" }, - { name = "gp-libs" }, - { name = "linkify-it-py" }, - { name = "myst-parser" }, - { name = "sphinx", specifier = "<9" }, - { name = "sphinx-argparse-neo", marker = "extra == 'argparse'", editable = "../gp-sphinx/packages/sphinx-argparse-neo" }, - { name = "sphinx-autodoc-typehints" }, - { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, - { name = "sphinx-fonts", editable = "../gp-sphinx/packages/sphinx-fonts" }, - { name = "sphinx-gptheme", editable = "../gp-sphinx/packages/sphinx-gptheme" }, - { name = "sphinx-inline-tabs" }, - { name = "sphinxext-opengraph" }, - { name = "sphinxext-rediraffe" }, +sdist = { url = "https://files.pythonhosted.org/packages/ed/04/c82ff029d74e0b0bf3e9ea29ec33af8036b07697ab9c5d96fd73ade46f38/gp_sphinx-0.0.1a7.tar.gz", hash = "sha256:c7eea8e35034a194848bb9102776aa11559a3545883f478f3c09b1a9beee06a4", size = 13992, upload-time = "2026-04-11T13:17:01.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/6b/01d8ab2777abeb83c34c9ddd1a8eea0f49d68c3ed95502ed50e666c71bcf/gp_sphinx-0.0.1a7-py3-none-any.whl", hash = "sha256:c8fda26b6a7213c4774449380059937f28b8e57190474fe2a2f691663a0b5212", size = 14411, upload-time = "2026-04-11T13:16:46.317Z" }, ] -provides-extras = ["argparse"] [[package]] name = "h11" @@ -1130,7 +1115,7 @@ dev = [ { name = "codecov" }, { name = "coverage" }, { name = "gp-libs" }, - { name = "gp-sphinx", editable = "../gp-sphinx/packages/gp-sphinx" }, + { name = "gp-sphinx", specifier = "==0.0.1a7" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1140,16 +1125,16 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "sphinx-autobuild" }, - { name = "sphinx-autodoc-api-style", editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" }, - { name = "sphinx-autodoc-fastmcp", editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" }, + { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a7" }, + { name = "sphinx-autodoc-fastmcp", specifier = "==0.0.1a7" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] docs = [ { name = "gp-libs" }, - { name = "gp-sphinx", editable = "../gp-sphinx/packages/gp-sphinx" }, + { name = "gp-sphinx", specifier = "==0.0.1a7" }, { name = "sphinx-autobuild" }, - { name = "sphinx-autodoc-api-style", editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" }, - { name = "sphinx-autodoc-fastmcp", editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" }, + { name = "sphinx-autodoc-api-style", specifier = "==0.0.1a7" }, + { name = "sphinx-autodoc-fastmcp", specifier = "==0.0.1a7" }, ] lint = [ { name = "mypy" }, @@ -2305,46 +2290,43 @@ wheels = [ [[package]] name = "sphinx-autodoc-api-style" -version = "0.0.1a6" -source = { editable = "../gp-sphinx/packages/sphinx-autodoc-api-style" } +version = "0.0.1a7" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-autodoc-badges" }, ] - -[package.metadata] -requires-dist = [ - { name = "sphinx" }, - { name = "sphinx-autodoc-badges", editable = "../gp-sphinx/packages/sphinx-autodoc-badges" }, +sdist = { url = "https://files.pythonhosted.org/packages/a2/ba/ac334df39fe2f25f7d5aa5bfc3cfe3ff1cda611f233bcd12118809fba564/sphinx_autodoc_api_style-0.0.1a7.tar.gz", hash = "sha256:8860616f0af7c8bfd340f65008c994e30bbf73a6fd3d851b3f181fceb664580a", size = 10923, upload-time = "2026-04-11T13:17:03.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/a3/ffb88b803d88374d2a0a361c5b82819a6e0fcbeebe16d368f81750dbc7a5/sphinx_autodoc_api_style-0.0.1a7-py3-none-any.whl", hash = "sha256:4627a148bab6889a0e2ec1b93c4ab12ee0438f04d6c8fbc350eda5c571f531cc", size = 11475, upload-time = "2026-04-11T13:16:49.713Z" }, ] [[package]] name = "sphinx-autodoc-badges" -version = "0.0.1a6" -source = { editable = "../gp-sphinx/packages/sphinx-autodoc-badges" } +version = "0.0.1a7" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] - -[package.metadata] -requires-dist = [{ name = "sphinx" }] +sdist = { url = "https://files.pythonhosted.org/packages/f8/23/561cf78ae0b5891cf6722f749c36caaf656aa64b481b37a121414ac890d7/sphinx_autodoc_badges-0.0.1a7.tar.gz", hash = "sha256:7aa04ad728d59023b65a174512497915bc2a9ab6d3160457c4a709ba88d31666", size = 8044, upload-time = "2026-04-11T13:17:04.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/6f/0c8100492c8567a6e1cd93b76834387e86947eda4e152357798d389d9c61/sphinx_autodoc_badges-0.0.1a7-py3-none-any.whl", hash = "sha256:902f5618cbec522f7aaad64c4fc613238bc3e9faa6085091000adc41eb95aa4d", size = 8365, upload-time = "2026-04-11T13:16:51.268Z" }, +] [[package]] name = "sphinx-autodoc-fastmcp" -version = "0.0.1a6" -source = { editable = "../gp-sphinx/packages/sphinx-autodoc-fastmcp" } +version = "0.0.1a7" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-autodoc-badges" }, ] - -[package.metadata] -requires-dist = [ - { name = "sphinx" }, - { name = "sphinx-autodoc-badges", editable = "../gp-sphinx/packages/sphinx-autodoc-badges" }, +sdist = { url = "https://files.pythonhosted.org/packages/b6/f1/a1b61f98d1dfba042bacffc823f7afea8c5b0a4a088f3896ae88d11f4636/sphinx_autodoc_fastmcp-0.0.1a7.tar.gz", hash = "sha256:3788cfc26bdea6fc1b6555aa94b24700f054c1618390950c1c2786cd49456b71", size = 14891, upload-time = "2026-04-11T13:17:06.645Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/95/ecbc501d5f1f1d8dc865447ca6a91f494bdaf61c3d508f0814cc14f4c304/sphinx_autodoc_fastmcp-0.0.1a7-py3-none-any.whl", hash = "sha256:c282c46de60f27ba3970387ca2d721d9bac87d44e3a3e2708daa9873c00366ad", size = 17521, upload-time = "2026-04-11T13:16:54.057Z" }, ] [[package]] @@ -2437,26 +2419,28 @@ wheels = [ [[package]] name = "sphinx-fonts" -version = "0.0.1a6" -source = { editable = "../gp-sphinx/packages/sphinx-fonts" } +version = "0.0.1a7" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] - -[package.metadata] -requires-dist = [{ name = "sphinx" }] +sdist = { url = "https://files.pythonhosted.org/packages/f6/8a/ea86daed70e0039aace2b8143610efebc1f8ce949c365e3907b2a0f58092/sphinx_fonts-0.0.1a7.tar.gz", hash = "sha256:7da3f383a225b623d38c263b3e805620fd0d9b262aa1f3a66bc9bbac2ba44a0b", size = 5624, upload-time = "2026-04-11T13:17:09.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/7c/a045b2021cc717cd474378e305e8df4f7b1a0971ef34096cbda8e0bd1c43/sphinx_fonts-0.0.1a7-py3-none-any.whl", hash = "sha256:68c109eb6a9b521e9d9105a08fd89b8dfd1012a058d9fcab49cfb05bd32eec11", size = 4348, upload-time = "2026-04-11T13:16:58.601Z" }, +] [[package]] name = "sphinx-gptheme" -version = "0.0.1a6" -source = { editable = "../gp-sphinx/packages/sphinx-gptheme" } +version = "0.0.1a7" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "furo" }, ] - -[package.metadata] -requires-dist = [{ name = "furo" }] +sdist = { url = "https://files.pythonhosted.org/packages/98/4d/277288688e242b96458ad79f07ce1a003c7d65b9f09c616337b799db8524/sphinx_gptheme-0.0.1a7.tar.gz", hash = "sha256:3b2dee7cdfe5206e0cd83d2ad9d0d44eb802fb0da4cc189b34a8d56ef9770ad6", size = 14569, upload-time = "2026-04-11T13:17:10.676Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/34/5a88f8f90fd7f70a89834b386be91f110bec12726e747e1c483cb1cccf50/sphinx_gptheme-0.0.1a7-py3-none-any.whl", hash = "sha256:fc2c61d96e3a65c628ed0bc62b414d7cc69089a5be8873f500e6c8ef1a833cc0", size = 15628, upload-time = "2026-04-11T13:17:00.123Z" }, +] [[package]] name = "sphinx-inline-tabs" From 4736b711ebf7a23e93853c2b88438dc31e1af01e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 11 Apr 2026 08:58:14 -0500 Subject: [PATCH 9/9] docs(CHANGES) Visual API docs improvements via gp-sphinx why: Capture the api-styling migration to the gp-sphinx package stack in the unreleased notes. what: - Add a Documentation bullet under libtmux-mcp 0.1.x linking to the project PR --- CHANGES | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES b/CHANGES index 3b5594b..44ea9e7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,15 @@ # Changelog +## libtmux-mcp 0.1.x (unreleased) + + +_Notes on upcoming releases will be added here_ + + +### Documentation + +- Visual improvements to API docs from [gp-sphinx](https://gp-sphinx.git-pull.com)-based Sphinx packages (#10) + ## libtmux-mcp 0.1.0a0 (2026-03-22) ### New features