From f0ee3d0a12ad24a5718cb2bf881cffa318249d6d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 8 Apr 2026 18:04:40 -0500 Subject: [PATCH 1/4] feat(docs): Add resource and model autodoc directives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix _MODEL_CLASSES staleness — replace hardcoded 10-model set with dynamic discovery via _discover_model_classes() using Pydantic introspection + __module__ filtering. PaneSnapshot and ContentChangeResult now get correct cross-references. Add 5 new directives: fastmcp-resource, fastmcp-resourcesummary, fastmcp-model, fastmcp-model-fields, fastmcp-modelsummary. Add 4 new roles: {resource}, {resourceref}, {model}, {modelref}. Add 4 new custom nodes with blue (resource) and purple (model) badges. Resource collection uses _ResourceCollector (mirrors _ToolCollector) to capture URI templates, titles, and params from hierarchy.py without executing tmux code. Model collection introspects BaseModel subclasses via model_fields with proper default_factory handling. Replace automodule dumps in reference/api/resources.md and models.md with structured autodoc output. Add resource and model role demos. Tests: 44 new tests across 3 files (251 total). --- docs/_ext/fastmcp_autodoc.py | 944 +++++++++++++++++++++- docs/_static/css/fastmcp_autodoc.css | 25 + docs/demo.md | 20 + docs/reference/api/models.md | 42 +- docs/reference/api/resources.md | 30 +- tests/docs/_ext/test_fastmcp_autodoc.py | 23 + tests/docs/_ext/test_fastmcp_models.py | 280 +++++++ tests/docs/_ext/test_fastmcp_resources.py | 202 +++++ 8 files changed, 1542 insertions(+), 24 deletions(-) create mode 100644 docs/_static/css/fastmcp_autodoc.css create mode 100644 tests/docs/_ext/test_fastmcp_models.py create mode 100644 tests/docs/_ext/test_fastmcp_resources.py diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index a21f185..cf493ce 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -65,18 +65,35 @@ TAG_DESTRUCTIVE = "destructive" _MODEL_MODULE = "libtmux_mcp.models" -_MODEL_CLASSES: set[str] = { - "SessionInfo", - "WindowInfo", - "PaneInfo", - "PaneContentMatch", - "ServerInfo", - "OptionResult", - "OptionSetResult", - "EnvironmentResult", - "EnvironmentSetResult", - "WaitForTextResult", -} +_model_classes_cache: set[str] | None = None + + +def _discover_model_classes() -> set[str]: + """Discover all BaseModel subclasses in libtmux_mcp.models. + + Results are cached after first call. Only discovers classes whose + ``__module__`` matches ``_MODEL_MODULE`` to prevent third-party leakage. + """ + global _model_classes_cache + if _model_classes_cache is not None: + return _model_classes_cache + import inspect as _inspect + + from pydantic import BaseModel + + try: + mod = importlib.import_module(_MODEL_MODULE) + except ImportError: + logger.warning("fastmcp_autodoc: could not import %s", _MODEL_MODULE) + _model_classes_cache = set() + return _model_classes_cache + _model_classes_cache = { + name + for name, obj in _inspect.getmembers(mod, _inspect.isclass) + if issubclass(obj, BaseModel) + and getattr(obj, "__module__", "") == _MODEL_MODULE + } + return _model_classes_cache # --------------------------------------------------------------------------- @@ -111,6 +128,40 @@ class ToolInfo: return_annotation: str +@dataclass +class ResourceInfo: + """Collected metadata for a single MCP resource.""" + + name: str + qualified_name: str + title: str + uri_template: str + docstring: str + params: list[ParamInfo] + return_annotation: str + + +@dataclass +class ModelFieldInfo: + """Extracted field information for a Pydantic model.""" + + name: str + type_str: str + required: bool + default: str + description: str + + +@dataclass +class ModelInfo: + """Collected metadata for a single Pydantic model.""" + + name: str + qualified_name: str + docstring: str + fields: list[ModelFieldInfo] + + # --------------------------------------------------------------------------- # Docstring + signature parsing # --------------------------------------------------------------------------- @@ -302,7 +353,7 @@ def _single_type_xref(name: str) -> addnodes.pending_xref: 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 + target = f"{_MODEL_MODULE}.{name}" if name in _discover_model_classes() else name return addnodes.pending_xref( "", nodes.literal("", name), @@ -459,6 +510,76 @@ def _safety_badge(safety: str) -> _safety_badge_node: return badge +class _resource_badge_node(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] + """Custom node for resource badges with ARIA attributes in HTML output.""" + + +def _visit_resource_badge_html(self: t.Any, node: _resource_badge_node) -> None: + """Emit opening ```` with classes, role, and aria-label.""" + classes = " ".join(node.get("classes", [])) + self.body.append( + f'' + ) + + +def _depart_resource_badge_html(self: t.Any, node: _resource_badge_node) -> None: + """Close the ````.""" + self.body.append("") + + +def _resource_badge() -> _resource_badge_node: + """Create a blue resource badge node with ARIA attributes.""" + _base = ["sd-sphinx-override", "sd-badge"] + badge = _resource_badge_node( + "", + nodes.Text("resource"), + classes=[*_base, "sd-bg-info", "sd-bg-text-info"], + ) + return badge + + +class _model_badge_node(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] + """Custom node for model badges with ARIA attributes in HTML output.""" + + +def _visit_model_badge_html(self: t.Any, node: _model_badge_node) -> None: + """Emit opening ```` with classes, role, and aria-label.""" + classes = " ".join(node.get("classes", [])) + self.body.append(f'') + + +def _depart_model_badge_html(self: t.Any, node: _model_badge_node) -> None: + """Close the ````.""" + self.body.append("") + + +def _model_badge() -> _model_badge_node: + """Create a purple model badge node with ARIA attributes.""" + _base = ["sd-sphinx-override", "sd-badge"] + badge = _model_badge_node( + "", + nodes.Text("model"), + classes=[*_base, "sd-bg-primary", "sd-bg-text-primary"], + ) + return badge + + +class _resource_ref_placeholder(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] + """Placeholder node for ``{resource}`` and ``{resourceref}`` roles. + + Resolved at ``doctree-resolved`` by ``_resolve_resource_refs``. + The ``show_badge`` attribute controls whether the resource badge is appended. + """ + + +class _model_ref_placeholder(nodes.General, nodes.Inline, nodes.Element): # type: ignore[misc] + """Placeholder node for ``{model}`` and ``{modelref}`` roles. + + Resolved at ``doctree-resolved`` by ``_resolve_model_refs``. + The ``show_badge`` attribute controls whether the model badge is appended. + """ + + # --------------------------------------------------------------------------- # Tool collection (runs at builder-inited) # --------------------------------------------------------------------------- @@ -512,6 +633,38 @@ def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: return decorator +class _ResourceCollector: + """Mock FastMCP that captures resource registrations.""" + + def __init__(self) -> None: + self.resources: list[ResourceInfo] = [] + self._current_module: str = "" + + def resource( + self, + uri_template: str, + title: str = "", + **kwargs: t.Any, + ) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]: + def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + self.resources.append( + ResourceInfo( + name=func.__name__, + qualified_name=f"{self._current_module}.{func.__name__}", + title=title or func.__name__.replace("_", " ").title(), + uri_template=uri_template, + 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() @@ -541,6 +694,111 @@ def _collect_tools(app: Sphinx) -> None: app.env.fastmcp_tools = {tool.name: tool for tool in collector.tools} # type: ignore[attr-defined] +def _collect_resources(app: Sphinx) -> None: + """Collect resource metadata from libtmux_mcp source at build time.""" + collector = _ResourceCollector() + + resource_modules = ["hierarchy"] + + for mod_name in resource_modules: + collector._current_module = mod_name + try: + mod = importlib.import_module(f"libtmux_mcp.resources.{mod_name}") + if hasattr(mod, "register"): + mod.register(collector) + except Exception: + logger.warning( + "fastmcp_autodoc: failed to load resource module %s", + mod_name, + exc_info=True, + ) + + app.env.fastmcp_resources = {r.name: r for r in collector.resources} # type: ignore[attr-defined] + + +def _collect_models(app: Sphinx) -> None: + """Collect Pydantic model metadata from libtmux_mcp.models at build time.""" + from pydantic import BaseModel + + try: + mod = importlib.import_module(_MODEL_MODULE) + except ImportError: + logger.warning("fastmcp_autodoc: could not import %s", _MODEL_MODULE) + app.env.fastmcp_models = {} # type: ignore[attr-defined] + return + + models: dict[str, ModelInfo] = {} + for name, obj in inspect.getmembers(mod, inspect.isclass): + if not issubclass(obj, BaseModel): + continue + if getattr(obj, "__module__", "") != _MODEL_MODULE: + continue + + fields: list[ModelFieldInfo] = [] + for field_name, field_info in obj.model_fields.items(): + # Determine type string + ann = obj.__annotations__.get(field_name, "") + type_str = _format_annotation(ann) + + # Determine required / default + has_default_factory = ( + hasattr(field_info, "default_factory") + and field_info.default_factory is not None + ) + has_default = not field_info.is_required() and not has_default_factory + + if has_default_factory: + required = False + factory = field_info.default_factory + # Show factory name for common factories + default_str = f"{factory.__name__}()" if factory else "" + elif has_default: + required = False + default_val = field_info.default + if default_val is None: + default_str = "None" + elif isinstance(default_val, bool): + default_str = str(default_val) + elif isinstance(default_val, str): + default_str = repr(default_val) + else: + default_str = str(default_val) + else: + required = True + default_str = "" + + # Extract description from Field(description=...) + description = "" + if hasattr(field_info, "description") and field_info.description: + description = field_info.description + + fields.append( + ModelFieldInfo( + name=field_name, + type_str=type_str, + required=required, + default=default_str, + description=description, + ) + ) + + models[name] = ModelInfo( + name=name, + qualified_name=f"{_MODEL_MODULE}.{name}", + docstring=obj.__doc__ or "", + fields=fields, + ) + + app.env.fastmcp_models = models # type: ignore[attr-defined] + + +def _collect_all(app: Sphinx) -> None: + """Collect tools, resources, and models at build time.""" + _collect_tools(app) + _collect_resources(app) + _collect_models(app) + + # --------------------------------------------------------------------------- # Directives # --------------------------------------------------------------------------- @@ -790,6 +1048,410 @@ def run(self) -> list[nodes.Node]: return result_nodes +class FastMCPResourceDirective(SphinxDirective): + """Autodocument a single MCP resource as a proper section. + + Creates a section node (visible in ToC) containing: + - Resource badge + one-line description + - URI template literal block + - Optional parameter table + - Return type + + Usage:: + + ```{fastmcp-resource} hierarchy.get_sessions + ``` + """ + + required_arguments = 1 + optional_arguments = 0 + has_content = True + final_argument_whitespace = False + + def run(self) -> list[nodes.Node]: + """Build resource section nodes.""" + arg = self.arguments[0] + func_name = arg.split(".")[-1] if "." in arg else arg + + resources: dict[str, ResourceInfo] = getattr(self.env, "fastmcp_resources", {}) + resource = resources.get(func_name) + + if resource is None: + return [ + self.state.document.reporter.warning( + f"fastmcp-resource: resource '{func_name}' not found. " + f"Available: {', '.join(sorted(resources.keys()))}", + line=self.lineno, + ) + ] + + return self._build_resource_section(resource) + + def _build_resource_section(self, resource: ResourceInfo) -> list[nodes.Node]: + """Build section: title, badge, description, URI template, params.""" + document = self.state.document + + # Section with anchor ID + section_id = f"resource-{resource.name.replace('_', '-')}" + section = nodes.section() + section["ids"].append(section_id) + document.note_explicit_target(section) + + # Title: resource name + resource badge + title_node = nodes.title("", "") + title_node += nodes.literal("", resource.name) + title_node += nodes.Text(" ") + title_node += _resource_badge() + section += title_node + + # Description paragraph + first_para = _first_paragraph(resource.docstring) + if first_para: + desc_para = _parse_rst_inline(first_para, self.state, self.lineno) + section += desc_para + + # URI template as literal block + uri_block = nodes.literal_block("", resource.uri_template) + uri_block["language"] = "none" + uri_block["classes"].append("fastmcp-uri-template") + section += uri_block + + # Returns + if resource.return_annotation: + returns_para = nodes.paragraph("") + returns_para += nodes.strong("", "Returns: ") + type_para = _make_type_xref(resource.return_annotation) + for child in type_para.children: + returns_para += child.deepcopy() + section += returns_para + + # Parameter table + if resource.params: + section += _make_para(nodes.strong("", "Parameters")) + headers = ["Parameter", "Type", "Required", "Default", "Description"] + rows: list[list[str | nodes.Node]] = [] + for p in resource.params: + desc_node = ( + _parse_rst_inline(p.description, self.state, self.lineno) + if p.description + else nodes.paragraph("", "\u2014") + ) + + type_cell, _is_enum = _make_type_cell_smart(p.type_str) + + default_cell: str | nodes.Node = "\u2014" + 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, + ] + ) + section += _make_table(headers, rows, col_widths=[15, 15, 8, 10, 52]) + + return [section] + + +class FastMCPResourceSummaryDirective(SphinxDirective): + """Generate a summary table of all resources. + + Produces a single table with URI Template, Title, and Description columns. + + Usage:: + + ```{fastmcp-resourcesummary} + ``` + """ + + required_arguments = 0 + optional_arguments = 0 + has_content = False + + def run(self) -> list[nodes.Node]: + """Build resource summary table.""" + resources: dict[str, ResourceInfo] = getattr(self.env, "fastmcp_resources", {}) + + if not resources: + return [ + self.state.document.reporter.warning( + "fastmcp-resourcesummary: no resources found.", + line=self.lineno, + ) + ] + + headers = ["URI Template", "Title", "Description"] + rows: list[list[str | nodes.Node]] = [] + for resource in sorted(resources.values(), key=lambda r: r.uri_template): + first_line = _first_paragraph(resource.docstring) + ref = nodes.reference("", "", internal=True) + section_id = f"resource-{resource.name.replace('_', '-')}" + ref["refuri"] = f"#{section_id}" + ref += nodes.literal("", resource.uri_template) + rows.append( + [ + _make_para(ref), + resource.title, + _parse_rst_inline(first_line, self.state, self.lineno), + ] + ) + + return [_make_table(headers, rows, col_widths=[35, 15, 50])] + + +class FastMCPModelDirective(SphinxDirective): + """Autodocument a single Pydantic model as a proper section. + + Creates a section node (visible in ToC) containing: + - Model badge + docstring + - Field table (Field, Type, Required, Default, Description) + + Options: + - ``:fields:`` — comma-separated allowlist of fields to include + - ``:exclude:`` — comma-separated denylist of fields to exclude + + Usage:: + + ```{fastmcp-model} SessionInfo + ``` + """ + + required_arguments = 1 + optional_arguments = 0 + has_content = True + final_argument_whitespace = False + option_spec: t.ClassVar[dict[str, t.Any]] = { + "fields": lambda x: x, + "exclude": lambda x: x, + } + + def run(self) -> list[nodes.Node]: + """Build model section nodes.""" + model_name = self.arguments[0].strip() + + models: dict[str, ModelInfo] = getattr(self.env, "fastmcp_models", {}) + model = models.get(model_name) + + if model is None: + return [ + self.state.document.reporter.warning( + f"fastmcp-model: model '{model_name}' not found. " + f"Available: {', '.join(sorted(models.keys()))}", + line=self.lineno, + ) + ] + + return self._build_model_section(model) + + def _build_model_section(self, model: ModelInfo) -> list[nodes.Node]: + """Build section: title, badge, docstring, field table.""" + document = self.state.document + + # Section with anchor ID + section_id = f"model-{model.name}" + section = nodes.section() + section["ids"].append(section_id) + document.note_explicit_target(section) + + # Title: model name + model badge + title_node = nodes.title("", "") + title_node += nodes.literal("", model.name) + title_node += nodes.Text(" ") + title_node += _model_badge() + section += title_node + + # Docstring + first_para = _first_paragraph(model.docstring) + if first_para: + desc_para = _parse_rst_inline(first_para, self.state, self.lineno) + section += desc_para + + # Field table + fields = self._filter_fields(model.fields) + if fields: + section += self._build_field_table(fields) + + return [section] + + def _filter_fields(self, fields: list[ModelFieldInfo]) -> list[ModelFieldInfo]: + """Apply :fields: and :exclude: options.""" + result = list(fields) + fields_opt = self.options.get("fields") + if fields_opt: + allow = {f.strip() for f in fields_opt.split(",")} + result = [f for f in result if f.name in allow] + exclude_opt = self.options.get("exclude") + if exclude_opt: + deny = {f.strip() for f in exclude_opt.split(",")} + result = [f for f in result if f.name not in deny] + return result + + def _build_field_table(self, fields: list[ModelFieldInfo]) -> nodes.table: + """Build a field table.""" + headers = ["Field", "Type", "Required", "Default", "Description"] + rows: list[list[str | nodes.Node]] = [] + for f in fields: + type_cell, _is_enum = _make_type_cell_smart(f.type_str) + + default_cell: str | nodes.Node = "\u2014" + if f.default and f.default != "None": + default_cell = _make_para(_make_literal(f.default)) + + desc = f.description or "\u2014" + + rows.append( + [ + _make_para(_make_literal(f.name)), + type_cell, + "yes" if f.required else "no", + default_cell, + desc, + ] + ) + return _make_table(headers, rows, col_widths=[15, 15, 8, 10, 52]) + + +class FastMCPModelFieldsDirective(SphinxDirective): + """Emit the field table for a model without a section wrapper. + + Useful for embedding model fields inline in other content. + + Options: + - ``:fields:`` — comma-separated allowlist of fields to include + - ``:exclude:`` — comma-separated denylist of fields to exclude + - ``:link-header:`` — if set, adds a header linking to the model section + + Usage:: + + ```{fastmcp-model-fields} SessionInfo + ``` + """ + + required_arguments = 1 + optional_arguments = 0 + has_content = False + option_spec: t.ClassVar[dict[str, t.Any]] = { + "fields": lambda x: x, + "exclude": lambda x: x, + "link-header": lambda x: x, + } + + def run(self) -> list[nodes.Node]: + """Build field table nodes.""" + model_name = self.arguments[0].strip() + + models: dict[str, ModelInfo] = getattr(self.env, "fastmcp_models", {}) + model = models.get(model_name) + + if model is None: + return [ + self.state.document.reporter.warning( + f"fastmcp-model-fields: model '{model_name}' not found.", + line=self.lineno, + ) + ] + + result: list[nodes.Node] = [] + + # Optional link header + link_header = self.options.get("link-header") + if link_header is not None: + ref = nodes.reference("", "", internal=True) + section_id = f"model-{model.name}" + ref["refuri"] = f"#{section_id}" + ref += nodes.literal("", model.name) + result.append(_make_para(ref)) + + # Filter and build table + fields = self._filter_fields(model.fields) + if fields: + headers = ["Field", "Type", "Required", "Default", "Description"] + rows: list[list[str | nodes.Node]] = [] + for f in fields: + type_cell, _is_enum = _make_type_cell_smart(f.type_str) + + default_cell: str | nodes.Node = "\u2014" + if f.default and f.default != "None": + default_cell = _make_para(_make_literal(f.default)) + + desc = f.description or "\u2014" + + rows.append( + [ + _make_para(_make_literal(f.name)), + type_cell, + "yes" if f.required else "no", + default_cell, + desc, + ] + ) + result.append(_make_table(headers, rows, col_widths=[15, 15, 8, 10, 52])) + + return result + + def _filter_fields(self, fields: list[ModelFieldInfo]) -> list[ModelFieldInfo]: + """Apply :fields: and :exclude: options.""" + result = list(fields) + fields_opt = self.options.get("fields") + if fields_opt: + allow = {f.strip() for f in fields_opt.split(",")} + result = [f for f in result if f.name in allow] + exclude_opt = self.options.get("exclude") + if exclude_opt: + deny = {f.strip() for f in exclude_opt.split(",")} + result = [f for f in result if f.name not in deny] + return result + + +class FastMCPModelSummaryDirective(SphinxDirective): + """Generate a summary table of all models. + + Produces a single table with Model and Description columns. + + Usage:: + + ```{fastmcp-modelsummary} + ``` + """ + + required_arguments = 0 + optional_arguments = 0 + has_content = False + + def run(self) -> list[nodes.Node]: + """Build model summary table.""" + models: dict[str, ModelInfo] = getattr(self.env, "fastmcp_models", {}) + + if not models: + return [ + self.state.document.reporter.warning( + "fastmcp-modelsummary: no models found.", + line=self.lineno, + ) + ] + + headers = ["Model", "Description"] + rows: list[list[str | nodes.Node]] = [] + for model in sorted(models.values(), key=lambda m: m.name): + first_line = _first_paragraph(model.docstring) + ref = nodes.reference("", "", internal=True) + section_id = f"model-{model.name}" + ref["refuri"] = f"#{section_id}" + ref += nodes.literal("", model.name) + rows.append( + [ + _make_para(ref), + _parse_rst_inline(first_line, self.state, self.lineno), + ] + ) + + return [_make_table(headers, rows, col_widths=[30, 70])] + + # --------------------------------------------------------------------------- # Extension setup # --------------------------------------------------------------------------- @@ -1059,16 +1721,248 @@ def _badge_role( return [_safety_badge(text.strip())], [] +def _resource_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 ``:resource:`get-sessions``` → linked name + resource badge.""" + target = text.strip().replace("_", "-") + node = _resource_ref_placeholder(rawtext, reftarget=target, show_badge=True) + return [node], [] + + +def _resourceref_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 ``:resourceref:`get-sessions``` → code-linked, no badge.""" + target = text.strip().replace("_", "-") + node = _resource_ref_placeholder(rawtext, reftarget=target, show_badge=False) + return [node], [] + + +def _model_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 ``:model:`SessionInfo``` → linked name + model badge.""" + target = text.strip() + node = _model_ref_placeholder(rawtext, reftarget=target, show_badge=True) + return [node], [] + + +def _modelref_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 ``:modelref:`SessionInfo``` → code-linked, no badge.""" + target = text.strip() + node = _model_ref_placeholder(rawtext, reftarget=target, show_badge=False) + return [node], [] + + +def _register_resource_labels(app: Sphinx, doctree: nodes.document) -> None: + """Register resource sections with StandardDomain for site-wide {ref} links. + + Same pattern as ``_register_tool_labels`` but for sections with + ``resource-`` prefixed IDs. + """ + 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 not section_id.startswith("resource-"): + continue + if section.children and isinstance(section[0], nodes.title): + title_node = section[0] + resource_name = "" + for child in title_node.children: + if isinstance(child, nodes.literal): + resource_name = child.astext() + break + if not resource_name: + continue + domain.anonlabels[section_id] = (docname, section_id) + domain.labels[section_id] = (docname, section_id, resource_name) + + +def _register_model_labels(app: Sphinx, doctree: nodes.document) -> None: + """Register model sections with StandardDomain for site-wide {ref} links. + + Same pattern as ``_register_tool_labels`` but for sections with + ``model-`` prefixed IDs. + """ + 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 not section_id.startswith("model-"): + continue + if section.children and isinstance(section[0], nodes.title): + title_node = section[0] + model_name = "" + for child in title_node.children: + if isinstance(child, nodes.literal): + model_name = child.astext() + break + if not model_name: + continue + domain.anonlabels[section_id] = (docname, section_id) + domain.labels[section_id] = (docname, section_id, model_name) + + +def _resolve_resource_refs( + app: Sphinx, + doctree: nodes.document, + fromdocname: str, +) -> None: + """Resolve ``{resource}`` and ``{resourceref}`` placeholders. + + ``{resource}`` renders as ``code`` + resource badge. + ``{resourceref}`` renders as ``code`` only (no badge). + """ + domain = t.cast("StandardDomain", app.env.get_domain("std")) + builder = app.builder + + for node in list(doctree.findall(_resource_ref_placeholder)): + target = node.get("reftarget", "") + show_badge = node.get("show_badge", True) + # Try resource-prefixed label + label_key = f"resource-{target}" + label_info = domain.labels.get(label_key) + if label_info is None: + node.replace_self(nodes.literal("", target.replace("-", "_"))) + continue + + todocname, labelid, _title = label_info + resource_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") + + newnode += nodes.literal("", resource_name) + if show_badge: + newnode += nodes.Text(" ") + newnode += _resource_badge() + + node.replace_self(newnode) + + +def _resolve_model_refs( + app: Sphinx, + doctree: nodes.document, + fromdocname: str, +) -> None: + """Resolve ``{model}`` and ``{modelref}`` placeholders. + + ``{model}`` renders as ``code`` + model badge. + ``{modelref}`` renders as ``code`` only (no badge). + """ + domain = t.cast("StandardDomain", app.env.get_domain("std")) + builder = app.builder + + for node in list(doctree.findall(_model_ref_placeholder)): + target = node.get("reftarget", "") + show_badge = node.get("show_badge", True) + # Try model-prefixed label + label_key = f"model-{target}" + label_info = domain.labels.get(label_key) + if label_info is None: + node.replace_self(nodes.literal("", target)) + continue + + todocname, labelid, _title = label_info + + 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") + + newnode += nodes.literal("", target) + if show_badge: + newnode += nodes.Text(" ") + newnode += _model_badge() + + node.replace_self(newnode) + + def setup(app: Sphinx) -> ExtensionMetadata: """Register the fastmcp_autodoc extension.""" + # Nodes app.add_node( _safety_badge_node, html=(_visit_safety_badge_html, _depart_safety_badge_html), ) - app.connect("builder-inited", _collect_tools) + app.add_node( + _resource_badge_node, + html=(_visit_resource_badge_html, _depart_resource_badge_html), + ) + app.add_node( + _model_badge_node, + html=(_visit_model_badge_html, _depart_model_badge_html), + ) + + # Collection + app.connect("builder-inited", _collect_all) + + # Label registration app.connect("doctree-read", _register_tool_labels) + app.connect("doctree-read", _register_resource_labels) + app.connect("doctree-read", _register_model_labels) + + # Ref resolution app.connect("doctree-resolved", _add_section_badges) app.connect("doctree-resolved", _resolve_tool_refs) + app.connect("doctree-resolved", _resolve_resource_refs) + app.connect("doctree-resolved", _resolve_model_refs) + + # Tool roles app.add_role("tool", _tool_role) app.add_role("toolref", _toolref_role) app.add_role("toolicon", _toolicon_role) @@ -1077,10 +1971,32 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_role("tooliconil", _tooliconil_role) app.add_role("tooliconir", _tooliconir_role) app.add_role("badge", _badge_role) + + # Resource roles + app.add_role("resource", _resource_role) + app.add_role("resourceref", _resourceref_role) + + # Model roles + app.add_role("model", _model_role) + app.add_role("modelref", _modelref_role) + + # Tool directives app.add_directive("fastmcp-tool", FastMCPToolDirective) app.add_directive("fastmcp-tool-input", FastMCPToolInputDirective) app.add_directive("fastmcp-toolsummary", FastMCPToolSummaryDirective) + # Resource directives + app.add_directive("fastmcp-resource", FastMCPResourceDirective) + app.add_directive("fastmcp-resourcesummary", FastMCPResourceSummaryDirective) + + # Model directives + app.add_directive("fastmcp-model", FastMCPModelDirective) + app.add_directive("fastmcp-model-fields", FastMCPModelFieldsDirective) + app.add_directive("fastmcp-modelsummary", FastMCPModelSummaryDirective) + + # CSS + app.add_css_file("css/fastmcp_autodoc.css") + return { "version": "0.1.0", "parallel_read_safe": True, diff --git a/docs/_static/css/fastmcp_autodoc.css b/docs/_static/css/fastmcp_autodoc.css new file mode 100644 index 0000000..14461e2 --- /dev/null +++ b/docs/_static/css/fastmcp_autodoc.css @@ -0,0 +1,25 @@ +/* fastmcp_autodoc.css — resource badge, model badge, URI template, table polish */ + +/* Resource badge (blue) — additional ARIA styling */ +span.sd-badge[aria-label*="resource"] { + user-select: none; +} + +/* Model badge (purple) — additional ARIA styling */ +span.sd-badge[aria-label*="model"] { + user-select: none; +} + +/* URI template block — left-border accent, monospace */ +.fastmcp-uri-template { + border-left: 3px solid var(--sd-color-info, #0dcaf0); + padding: 0.5em 1em; + background: var(--sd-color-info-bg, #f0f9ff); + font-family: var(--sd-fontfamily-monospace, monospace); +} + +/* Field/param table polish — tighter padding */ +.fastmcp-autodoc-table td, +.fastmcp-autodoc-table th { + padding: 0.35em 0.6em; +} diff --git a/docs/demo.md b/docs/demo.md index 2147547..7b45cfd 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -72,6 +72,26 @@ Use {tooliconl}`search-panes` to find text across all panes. If you know which p The fundamental pattern: {toolref}`send-keys` → {toolref}`wait-for-text` → {toolref}`capture-pane`. For discovery: {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`get-pane-info`. +## Resource references + +### `{resource}` — code-linked with badge + +{resource}`get-sessions` · {resource}`get-session` · {resource}`get-session-windows` · {resource}`get-window` · {resource}`get-pane` · {resource}`get-pane-content` + +### `{resourceref}` — code-linked, no badge + +{resourceref}`get-sessions` · {resourceref}`get-session` · {resourceref}`get-session-windows` · {resourceref}`get-window` · {resourceref}`get-pane` · {resourceref}`get-pane-content` + +## Model references + +### `{model}` — code-linked with badge + +{model}`SessionInfo` · {model}`WindowInfo` · {model}`PaneInfo` · {model}`PaneSnapshot` · {model}`ServerInfo` · {model}`WaitForTextResult` + +### `{modelref}` — code-linked, no badge + +{modelref}`SessionInfo` · {modelref}`WindowInfo` · {modelref}`PaneInfo` · {modelref}`PaneSnapshot` · {modelref}`ServerInfo` · {modelref}`WaitForTextResult` + ## Environment variable references {envvar}`LIBTMUX_SOCKET` · {envvar}`LIBTMUX_SAFETY` · {envvar}`LIBTMUX_SOCKET_PATH` · {envvar}`LIBTMUX_TMUX_BIN` diff --git a/docs/reference/api/models.md b/docs/reference/api/models.md index 57b63b9..34954af 100644 --- a/docs/reference/api/models.md +++ b/docs/reference/api/models.md @@ -1,8 +1,40 @@ # Models -```{eval-rst} -.. automodule:: libtmux_mcp.models - :members: - :undoc-members: - :show-inheritance: +```{fastmcp-modelsummary} +``` + +```{fastmcp-model} SessionInfo +``` + +```{fastmcp-model} WindowInfo +``` + +```{fastmcp-model} PaneInfo +``` + +```{fastmcp-model} PaneContentMatch +``` + +```{fastmcp-model} ServerInfo +``` + +```{fastmcp-model} OptionResult +``` + +```{fastmcp-model} OptionSetResult +``` + +```{fastmcp-model} EnvironmentResult +``` + +```{fastmcp-model} EnvironmentSetResult +``` + +```{fastmcp-model} WaitForTextResult +``` + +```{fastmcp-model} PaneSnapshot +``` + +```{fastmcp-model} ContentChangeResult ``` diff --git a/docs/reference/api/resources.md b/docs/reference/api/resources.md index 245d916..7f99e3f 100644 --- a/docs/reference/api/resources.md +++ b/docs/reference/api/resources.md @@ -1,8 +1,28 @@ # Resources -```{eval-rst} -.. automodule:: libtmux_mcp.resources.hierarchy - :members: - :undoc-members: - :show-inheritance: +```{fastmcp-resourcesummary} +``` + +## Session Resources + +```{fastmcp-resource} hierarchy.get_sessions +``` + +```{fastmcp-resource} hierarchy.get_session +``` + +```{fastmcp-resource} hierarchy.get_session_windows +``` + +## Window Resources + +```{fastmcp-resource} hierarchy.get_window +``` + +## Pane Resources + +```{fastmcp-resource} hierarchy.get_pane +``` + +```{fastmcp-resource} hierarchy.get_pane_content ``` diff --git a/tests/docs/_ext/test_fastmcp_autodoc.py b/tests/docs/_ext/test_fastmcp_autodoc.py index a2ed9fa..1876999 100644 --- a/tests/docs/_ext/test_fastmcp_autodoc.py +++ b/tests/docs/_ext/test_fastmcp_autodoc.py @@ -665,6 +665,29 @@ def test_collect_real_tools() -> None: assert socket_param.required is False +# --------------------------------------------------------------------------- +# _discover_model_classes +# --------------------------------------------------------------------------- + + +def test_discover_model_classes_finds_all_12() -> None: + """_discover_model_classes finds all 12 Pydantic models.""" + classes = fastmcp_autodoc._discover_model_classes() + assert len(classes) == 12 + + +def test_discover_model_classes_includes_pane_snapshot() -> None: + """_discover_model_classes includes PaneSnapshot (previously missing).""" + classes = fastmcp_autodoc._discover_model_classes() + assert "PaneSnapshot" in classes + + +def test_discover_model_classes_includes_content_change_result() -> None: + """_discover_model_classes includes ContentChangeResult (previously missing).""" + classes = fastmcp_autodoc._discover_model_classes() + assert "ContentChangeResult" in classes + + def test_collect_real_tools_total_count() -> None: """All 38 tools should be collected.""" collector = fastmcp_autodoc._ToolCollector() diff --git a/tests/docs/_ext/test_fastmcp_models.py b/tests/docs/_ext/test_fastmcp_models.py new file mode 100644 index 0000000..03abfa4 --- /dev/null +++ b/tests/docs/_ext/test_fastmcp_models.py @@ -0,0 +1,280 @@ +"""Tests for fastmcp_autodoc model collection and nodes.""" + +from __future__ import annotations + +import typing as t +from unittest.mock import MagicMock + +import fastmcp_autodoc +import pytest + +# --------------------------------------------------------------------------- +# _collect_models +# --------------------------------------------------------------------------- + + +def _collect_models_result() -> dict[str, fastmcp_autodoc.ModelInfo]: + """Run _collect_models and return the result dict.""" + app = MagicMock() + app.env = MagicMock() + fastmcp_autodoc._collect_models(app) + result: dict[str, fastmcp_autodoc.ModelInfo] = app.env.fastmcp_models + return result + + +def test_collect_models_discovers_all_12() -> None: + """_collect_models finds all 12 Pydantic models in libtmux_mcp.models.""" + models = _collect_models_result() + assert len(models) == 12 + + +class ModelNameFixture(t.NamedTuple): + """Test fixture for verifying model names.""" + + test_id: str + name: str + + +MODEL_NAME_FIXTURES: list[ModelNameFixture] = [ + ModelNameFixture(test_id="SessionInfo", name="SessionInfo"), + ModelNameFixture(test_id="WindowInfo", name="WindowInfo"), + ModelNameFixture(test_id="PaneInfo", name="PaneInfo"), + ModelNameFixture(test_id="PaneContentMatch", name="PaneContentMatch"), + ModelNameFixture(test_id="ServerInfo", name="ServerInfo"), + ModelNameFixture(test_id="OptionResult", name="OptionResult"), + ModelNameFixture(test_id="OptionSetResult", name="OptionSetResult"), + ModelNameFixture(test_id="EnvironmentResult", name="EnvironmentResult"), + ModelNameFixture(test_id="EnvironmentSetResult", name="EnvironmentSetResult"), + ModelNameFixture(test_id="WaitForTextResult", name="WaitForTextResult"), + ModelNameFixture(test_id="PaneSnapshot", name="PaneSnapshot"), + ModelNameFixture(test_id="ContentChangeResult", name="ContentChangeResult"), +] + + +@pytest.mark.parametrize( + MODEL_NAME_FIXTURES[0]._fields, + MODEL_NAME_FIXTURES, + ids=[f.test_id for f in MODEL_NAME_FIXTURES], +) +def test_collect_models_includes_model(test_id: str, name: str) -> None: + """_collect_models includes each expected model.""" + models = _collect_models_result() + assert name in models + + +# --------------------------------------------------------------------------- +# Field counts +# --------------------------------------------------------------------------- + + +class FieldCountFixture(t.NamedTuple): + """Test fixture for field count verification.""" + + test_id: str + model_name: str + expected_count: int + + +FIELD_COUNT_FIXTURES: list[FieldCountFixture] = [ + FieldCountFixture( + test_id="SessionInfo_5", + model_name="SessionInfo", + expected_count=5, + ), + FieldCountFixture( + test_id="PaneSnapshot_14", + model_name="PaneSnapshot", + expected_count=14, + ), + FieldCountFixture( + test_id="WindowInfo_10", + model_name="WindowInfo", + expected_count=10, + ), + FieldCountFixture( + test_id="PaneInfo_12", + model_name="PaneInfo", + expected_count=12, + ), + FieldCountFixture( + test_id="ContentChangeResult_4", + model_name="ContentChangeResult", + expected_count=4, + ), + FieldCountFixture( + test_id="WaitForTextResult_5", + model_name="WaitForTextResult", + expected_count=5, + ), +] + + +@pytest.mark.parametrize( + FIELD_COUNT_FIXTURES[0]._fields, + FIELD_COUNT_FIXTURES, + ids=[f.test_id for f in FIELD_COUNT_FIXTURES], +) +def test_model_field_count( + test_id: str, + model_name: str, + expected_count: int, +) -> None: + """Models have the expected number of fields.""" + models = _collect_models_result() + model = models[model_name] + assert len(model.fields) == expected_count + + +# --------------------------------------------------------------------------- +# Field description extraction +# --------------------------------------------------------------------------- + + +def test_field_description_extraction() -> None: + """Field(description=...) values are extracted correctly.""" + models = _collect_models_result() + session = models["SessionInfo"] + field_map = {f.name: f for f in session.fields} + + assert "session_id" in field_map + assert field_map["session_id"].description == "Session ID (e.g. '$1')" + + assert "window_count" in field_map + assert field_map["window_count"].description == "Number of windows" + + +# --------------------------------------------------------------------------- +# Required vs optional detection +# --------------------------------------------------------------------------- + + +def test_required_vs_optional_detection() -> None: + """Required fields and optional fields are distinguished correctly.""" + models = _collect_models_result() + session = models["SessionInfo"] + field_map = {f.name: f for f in session.fields} + + # session_id has no default → required + assert field_map["session_id"].required is True + assert field_map["session_id"].default == "" + + # session_name has default=None → optional + assert field_map["session_name"].required is False + assert field_map["session_name"].default == "None" + + # window_count has no default → required + assert field_map["window_count"].required is True + + +# --------------------------------------------------------------------------- +# default_factory handling +# --------------------------------------------------------------------------- + + +def test_default_factory_handling() -> None: + """WaitForTextResult.matched_lines uses default_factory=list.""" + models = _collect_models_result() + wait_result = models["WaitForTextResult"] + field_map = {f.name: f for f in wait_result.fields} + + matched_lines = field_map["matched_lines"] + assert matched_lines.required is False + assert matched_lines.default == "list()" + + +# --------------------------------------------------------------------------- +# _model_badge_node +# --------------------------------------------------------------------------- + + +def test_model_badge_classes() -> None: + """_model_badge creates badge node with correct CSS classes.""" + badge = fastmcp_autodoc._model_badge() + assert isinstance(badge, fastmcp_autodoc._model_badge_node) + assert "sd-bg-primary" in badge["classes"] + assert "sd-bg-text-primary" in badge["classes"] + assert "sd-sphinx-override" in badge["classes"] + assert "sd-badge" in badge["classes"] + assert badge.astext() == "model" + + +# --------------------------------------------------------------------------- +# Model roles +# --------------------------------------------------------------------------- + + +def test_model_role_creates_placeholder() -> None: + """_model_role creates _model_ref_placeholder with show_badge=True.""" + result_nodes, _messages = fastmcp_autodoc._model_role( + "model", ":model:`SessionInfo`", "SessionInfo", 1, None + ) + assert len(result_nodes) == 1 + node = result_nodes[0] + assert isinstance(node, fastmcp_autodoc._model_ref_placeholder) + assert node["reftarget"] == "SessionInfo" + assert node["show_badge"] is True + + +def test_modelref_role_creates_placeholder() -> None: + """_modelref_role creates _model_ref_placeholder with show_badge=False.""" + result_nodes, _messages = fastmcp_autodoc._modelref_role( + "modelref", ":modelref:`SessionInfo`", "SessionInfo", 1, None + ) + assert len(result_nodes) == 1 + node = result_nodes[0] + assert isinstance(node, fastmcp_autodoc._model_ref_placeholder) + assert node["reftarget"] == "SessionInfo" + assert node["show_badge"] is False + + +# --------------------------------------------------------------------------- +# :fields: and :exclude: filtering +# --------------------------------------------------------------------------- + + +def test_model_directive_fields_allowlist() -> None: + """FastMCPModelDirective :fields: option filters to allowed fields.""" + directive = fastmcp_autodoc.FastMCPModelDirective.__new__( + fastmcp_autodoc.FastMCPModelDirective + ) + directive.options = {"fields": "session_id, window_count"} + + all_fields = [ + fastmcp_autodoc.ModelFieldInfo("session_id", "str", True, "", ""), + fastmcp_autodoc.ModelFieldInfo("session_name", "str | None", False, "None", ""), + fastmcp_autodoc.ModelFieldInfo("window_count", "int", True, "", ""), + ] + + filtered = directive._filter_fields(all_fields) + names = [f.name for f in filtered] + assert names == ["session_id", "window_count"] + + +def test_model_directive_exclude_denylist() -> None: + """FastMCPModelDirective :exclude: option removes denied fields.""" + directive = fastmcp_autodoc.FastMCPModelDirective.__new__( + fastmcp_autodoc.FastMCPModelDirective + ) + directive.options = {"exclude": "session_name"} + + all_fields = [ + fastmcp_autodoc.ModelFieldInfo("session_id", "str", True, "", ""), + fastmcp_autodoc.ModelFieldInfo("session_name", "str | None", False, "None", ""), + fastmcp_autodoc.ModelFieldInfo("window_count", "int", True, "", ""), + ] + + filtered = directive._filter_fields(all_fields) + names = [f.name for f in filtered] + assert names == ["session_id", "window_count"] + + +# --------------------------------------------------------------------------- +# Qualified names +# --------------------------------------------------------------------------- + + +def test_model_qualified_name() -> None: + """Model qualified_name includes full module path.""" + models = _collect_models_result() + assert models["SessionInfo"].qualified_name == "libtmux_mcp.models.SessionInfo" + assert models["PaneSnapshot"].qualified_name == "libtmux_mcp.models.PaneSnapshot" diff --git a/tests/docs/_ext/test_fastmcp_resources.py b/tests/docs/_ext/test_fastmcp_resources.py new file mode 100644 index 0000000..e4b66d4 --- /dev/null +++ b/tests/docs/_ext/test_fastmcp_resources.py @@ -0,0 +1,202 @@ +"""Tests for fastmcp_autodoc resource collection and nodes.""" + +from __future__ import annotations + +import typing as t + +import fastmcp_autodoc +import pytest + +# --------------------------------------------------------------------------- +# _ResourceCollector +# --------------------------------------------------------------------------- + + +def test_resource_collector_captures_registrations() -> None: + """_ResourceCollector captures resource metadata from register() calls.""" + collector = fastmcp_autodoc._ResourceCollector() + collector._current_module = "hierarchy" + + @collector.resource("tmux://sessions{?socket_name}", title="All Sessions") + def get_sessions(socket_name: str | None = None) -> str: + """List all tmux sessions. + + Parameters + ---------- + socket_name : str, optional + tmux socket name. + + Returns + ------- + str + """ + return "" + + assert len(collector.resources) == 1 + resource = collector.resources[0] + assert resource.name == "get_sessions" + assert resource.qualified_name == "hierarchy.get_sessions" + assert resource.title == "All Sessions" + assert resource.uri_template == "tmux://sessions{?socket_name}" + assert len(resource.params) == 1 + assert resource.params[0].name == "socket_name" + assert resource.params[0].required is False + + +def test_resource_collector_default_title() -> None: + """_ResourceCollector uses func name as default title.""" + collector = fastmcp_autodoc._ResourceCollector() + collector._current_module = "hierarchy" + + @collector.resource("tmux://sessions") + def get_sessions() -> str: + """List sessions.""" + return "" + + assert collector.resources[0].title == "Get Sessions" + + +# --------------------------------------------------------------------------- +# Real resource collection +# --------------------------------------------------------------------------- + + +def test_collect_real_resources_total_count() -> None: + """All 6 resources should be collected from hierarchy.py.""" + collector = fastmcp_autodoc._ResourceCollector() + collector._current_module = "hierarchy" + + import importlib + + mod = importlib.import_module("libtmux_mcp.resources.hierarchy") + mod.register(collector) + + assert len(collector.resources) == 6 + + +class RealResourceFixture(t.NamedTuple): + """Test fixture for real resource verification.""" + + test_id: str + name: str + uri_template: str + title: str + + +REAL_RESOURCE_FIXTURES: list[RealResourceFixture] = [ + RealResourceFixture( + test_id="get_sessions", + name="get_sessions", + uri_template="tmux://sessions{?socket_name}", + title="All Sessions", + ), + RealResourceFixture( + test_id="get_session", + name="get_session", + uri_template="tmux://sessions/{session_name}{?socket_name}", + title="Session Detail", + ), + RealResourceFixture( + test_id="get_session_windows", + name="get_session_windows", + uri_template="tmux://sessions/{session_name}/windows{?socket_name}", + title="Session Windows", + ), + RealResourceFixture( + test_id="get_window", + name="get_window", + uri_template="tmux://sessions/{session_name}/windows/{window_index}{?socket_name}", + title="Window Detail", + ), + RealResourceFixture( + test_id="get_pane", + name="get_pane", + uri_template="tmux://panes/{pane_id}{?socket_name}", + title="Pane Detail", + ), + RealResourceFixture( + test_id="get_pane_content", + name="get_pane_content", + uri_template="tmux://panes/{pane_id}/content{?socket_name}", + title="Pane Content", + ), +] + + +@pytest.mark.parametrize( + REAL_RESOURCE_FIXTURES[0]._fields, + REAL_RESOURCE_FIXTURES, + ids=[f.test_id for f in REAL_RESOURCE_FIXTURES], +) +def test_collect_real_resource_details( + test_id: str, + name: str, + uri_template: str, + title: str, +) -> None: + """Real resources have correct URI templates and titles.""" + collector = fastmcp_autodoc._ResourceCollector() + collector._current_module = "hierarchy" + + import importlib + + mod = importlib.import_module("libtmux_mcp.resources.hierarchy") + mod.register(collector) + + resources = {r.name: r for r in collector.resources} + resource = resources[name] + assert resource.uri_template == uri_template + assert resource.title == title + + +# --------------------------------------------------------------------------- +# _resource_badge_node +# --------------------------------------------------------------------------- + + +def test_resource_badge_classes() -> None: + """_resource_badge creates badge node with correct CSS classes.""" + badge = fastmcp_autodoc._resource_badge() + assert isinstance(badge, fastmcp_autodoc._resource_badge_node) + assert "sd-bg-info" in badge["classes"] + assert "sd-bg-text-info" in badge["classes"] + assert "sd-sphinx-override" in badge["classes"] + assert "sd-badge" in badge["classes"] + assert badge.astext() == "resource" + + +# --------------------------------------------------------------------------- +# Resource roles +# --------------------------------------------------------------------------- + + +def test_resource_role_creates_placeholder() -> None: + """_resource_role creates _resource_ref_placeholder with show_badge=True.""" + result_nodes, _messages = fastmcp_autodoc._resource_role( + "resource", ":resource:`get-sessions`", "get-sessions", 1, None + ) + assert len(result_nodes) == 1 + node = result_nodes[0] + assert isinstance(node, fastmcp_autodoc._resource_ref_placeholder) + assert node["reftarget"] == "get-sessions" + assert node["show_badge"] is True + + +def test_resourceref_role_creates_placeholder() -> None: + """_resourceref_role creates _resource_ref_placeholder with show_badge=False.""" + result_nodes, _messages = fastmcp_autodoc._resourceref_role( + "resourceref", ":resourceref:`get-sessions`", "get-sessions", 1, None + ) + assert len(result_nodes) == 1 + node = result_nodes[0] + assert isinstance(node, fastmcp_autodoc._resource_ref_placeholder) + assert node["reftarget"] == "get-sessions" + assert node["show_badge"] is False + + +def test_resource_role_normalizes_underscores() -> None: + """_resource_role converts underscores to hyphens in target.""" + result_nodes, _ = fastmcp_autodoc._resource_role( + "resource", ":resource:`get_sessions`", "get_sessions", 1, None + ) + assert result_nodes[0]["reftarget"] == "get-sessions" From c097c53ba41d7aa506031105b856ffdd514e07ff Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 8 Apr 2026 18:29:07 -0500 Subject: [PATCH 2/4] docs(tools): Split tools into individual pages with grid navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move each of the 38 tools from 4 area pages (sessions.md, windows.md, panes.md, options.md) into individual pages under docs/tools/. Fix FastMCPToolSummaryDirective link generation to use anchor-only refs instead of hardcoded area paths, making it page-layout-agnostic. Add sphinxcontrib-rediraffe redirects for the 4 removed area pages pointing to tools/index where the grid cards provide navigation. Grid cards, {tool}/{toolref}/{tooliconl} roles, and all cross-references continue working unchanged — Sphinx resolves labels globally via StandardDomain regardless of which page contains the section. --- docs/_ext/fastmcp_autodoc.py | 4 +- docs/redirects.txt | 4 + docs/tools/capture-pane.md | 40 ++ docs/tools/clear-pane.md | 26 + docs/tools/create-session.md | 36 ++ docs/tools/create-window.md | 38 ++ docs/tools/display-message.md | 33 ++ docs/tools/enter-copy-mode.md | 44 ++ docs/tools/exit-copy-mode.md | 40 ++ docs/tools/get-pane-info.md | 42 ++ docs/tools/get-server-info.md | 33 ++ docs/tools/index.md | 42 +- docs/tools/kill-pane.md | 29 + docs/tools/kill-server.md | 27 + docs/tools/kill-session.md | 29 + docs/tools/kill-window.md | 28 + docs/tools/list-panes.md | 56 ++ docs/tools/list-sessions.md | 36 ++ docs/tools/list-windows.md | 54 ++ docs/tools/move-window.md | 39 ++ docs/tools/options.md | 138 ----- docs/tools/panes.md | 742 -------------------------- docs/tools/paste-text.md | 31 ++ docs/tools/pipe-pane.md | 33 ++ docs/tools/rename-session.md | 33 ++ docs/tools/rename-window.md | 38 ++ docs/tools/resize-pane.md | 40 ++ docs/tools/resize-window.md | 39 ++ docs/tools/search-panes.md | 47 ++ docs/tools/select-layout.md | 39 ++ docs/tools/select-pane.md | 42 ++ docs/tools/select-window.md | 39 ++ docs/tools/send-keys.md | 32 ++ docs/tools/sessions.md | 255 --------- docs/tools/set-environment.md | 31 ++ docs/tools/set-option.md | 32 ++ docs/tools/set-pane-title.md | 40 ++ docs/tools/show-environment.md | 32 ++ docs/tools/show-option.md | 30 ++ docs/tools/snapshot-pane.md | 47 ++ docs/tools/split-window.md | 41 ++ docs/tools/swap-pane.md | 41 ++ docs/tools/wait-for-content-change.md | 38 ++ docs/tools/wait-for-text.md | 42 ++ docs/tools/windows.md | 400 -------------- 45 files changed, 1461 insertions(+), 1541 deletions(-) create mode 100644 docs/tools/capture-pane.md create mode 100644 docs/tools/clear-pane.md create mode 100644 docs/tools/create-session.md create mode 100644 docs/tools/create-window.md create mode 100644 docs/tools/display-message.md create mode 100644 docs/tools/enter-copy-mode.md create mode 100644 docs/tools/exit-copy-mode.md create mode 100644 docs/tools/get-pane-info.md create mode 100644 docs/tools/get-server-info.md create mode 100644 docs/tools/kill-pane.md create mode 100644 docs/tools/kill-server.md create mode 100644 docs/tools/kill-session.md create mode 100644 docs/tools/kill-window.md create mode 100644 docs/tools/list-panes.md create mode 100644 docs/tools/list-sessions.md create mode 100644 docs/tools/list-windows.md create mode 100644 docs/tools/move-window.md delete mode 100644 docs/tools/options.md delete mode 100644 docs/tools/panes.md create mode 100644 docs/tools/paste-text.md create mode 100644 docs/tools/pipe-pane.md create mode 100644 docs/tools/rename-session.md create mode 100644 docs/tools/rename-window.md create mode 100644 docs/tools/resize-pane.md create mode 100644 docs/tools/resize-window.md create mode 100644 docs/tools/search-panes.md create mode 100644 docs/tools/select-layout.md create mode 100644 docs/tools/select-pane.md create mode 100644 docs/tools/select-window.md create mode 100644 docs/tools/send-keys.md delete mode 100644 docs/tools/sessions.md create mode 100644 docs/tools/set-environment.md create mode 100644 docs/tools/set-option.md create mode 100644 docs/tools/set-pane-title.md create mode 100644 docs/tools/show-environment.md create mode 100644 docs/tools/show-option.md create mode 100644 docs/tools/snapshot-pane.md create mode 100644 docs/tools/split-window.md create mode 100644 docs/tools/swap-pane.md create mode 100644 docs/tools/wait-for-content-change.md create mode 100644 docs/tools/wait-for-text.md delete mode 100644 docs/tools/windows.md diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index cf493ce..c6fddcb 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -1031,9 +1031,9 @@ def run(self) -> list[nodes.Node]: 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 + section_id = tool.name.replace("_", "-") ref = nodes.reference("", "", internal=True) - ref["refuri"] = f"{tool.area}/#{tool.name.replace('_', '-')}" + ref["refuri"] = f"#{section_id}" ref += nodes.literal("", tool.name) rows.append( [ diff --git a/docs/redirects.txt b/docs/redirects.txt index 1baa24d..4ac1aee 100644 --- a/docs/redirects.txt +++ b/docs/redirects.txt @@ -12,3 +12,7 @@ "concepts" "topics/concepts" "safety" "topics/safety" "guides/troubleshooting" "topics/troubleshooting" +"tools/sessions" "tools/index" +"tools/windows" "tools/index" +"tools/panes" "tools/index" +"tools/options" "tools/index" diff --git a/docs/tools/capture-pane.md b/docs/tools/capture-pane.md new file mode 100644 index 0000000..2b949d3 --- /dev/null +++ b/docs/tools/capture-pane.md @@ -0,0 +1,40 @@ +```{fastmcp-tool} pane_tools.capture_pane +``` + +**Use when** you need to read what's currently displayed in a terminal — +after running a command, checking output, or verifying state. + +**Avoid when** you need to search across multiple panes at once — use +{tooliconl}`search-panes`. If you only need pane metadata (not content), use +{tooliconl}`get-pane-info`. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "capture_pane", + "arguments": { + "pane_id": "%0", + "start": -50 + } +} +``` + +Response (string): + +```text +$ echo "Running tests..." +Running tests... +$ echo "PASS: test_auth (0.3s)" +PASS: test_auth (0.3s) +$ echo "FAIL: test_upload (AssertionError)" +FAIL: test_upload (AssertionError) +$ echo "3 tests: 2 passed, 1 failed" +3 tests: 2 passed, 1 failed +$ +``` + +```{fastmcp-tool-input} pane_tools.capture_pane +``` diff --git a/docs/tools/clear-pane.md b/docs/tools/clear-pane.md new file mode 100644 index 0000000..f670544 --- /dev/null +++ b/docs/tools/clear-pane.md @@ -0,0 +1,26 @@ +```{fastmcp-tool} pane_tools.clear_pane +``` + +**Use when** you want a clean terminal before capturing output. + +**Side effects:** Clears the pane's visible content. + +**Example:** + +```json +{ + "tool": "clear_pane", + "arguments": { + "pane_id": "%0" + } +} +``` + +Response (string): + +```text +Pane cleared: %0 +``` + +```{fastmcp-tool-input} pane_tools.clear_pane +``` diff --git a/docs/tools/create-session.md b/docs/tools/create-session.md new file mode 100644 index 0000000..fdc1a49 --- /dev/null +++ b/docs/tools/create-session.md @@ -0,0 +1,36 @@ +```{fastmcp-tool} server_tools.create_session +``` + +**Use when** you need a new isolated workspace. Sessions are the top-level +container — create one before creating windows or panes. + +**Avoid when** a session with the target name already exists — check with +{tooliconl}`list-sessions` first, or the command will fail. + +**Side effects:** Creates a new tmux session with one window and one pane. + +**Example:** + +```json +{ + "tool": "create_session", + "arguments": { + "session_name": "dev" + } +} +``` + +Response: + +```json +{ + "session_id": "$1", + "session_name": "dev", + "window_count": 1, + "session_attached": "0", + "session_created": "1774521872" +} +``` + +```{fastmcp-tool-input} server_tools.create_session +``` diff --git a/docs/tools/create-window.md b/docs/tools/create-window.md new file mode 100644 index 0000000..db6602a --- /dev/null +++ b/docs/tools/create-window.md @@ -0,0 +1,38 @@ +```{fastmcp-tool} session_tools.create_window +``` + +**Use when** you need a new terminal workspace within an existing session. + +**Side effects:** Creates a new window. Attaches to it if `attach` is true. + +**Example:** + +```json +{ + "tool": "create_window", + "arguments": { + "session_name": "dev", + "window_name": "logs" + } +} +``` + +Response: + +```json +{ + "window_id": "@2", + "window_name": "logs", + "window_index": "3", + "session_id": "$0", + "session_name": "dev", + "pane_count": 1, + "window_layout": "b25f,80x24,0,0,5", + "window_active": "1", + "window_width": "80", + "window_height": "24" +} +``` + +```{fastmcp-tool-input} session_tools.create_window +``` diff --git a/docs/tools/display-message.md b/docs/tools/display-message.md new file mode 100644 index 0000000..1c7c0f6 --- /dev/null +++ b/docs/tools/display-message.md @@ -0,0 +1,33 @@ +```{fastmcp-tool} pane_tools.display_message +``` + +**Use when** you need to query arbitrary tmux variables — zoom state, pane +dead flag, client activity, or any `#{format}` string that isn't covered by +other tools. + +**Avoid when** a dedicated tool already provides the information — e.g. use +{tooliconl}`snapshot-pane` for cursor position and mode, or +{tooliconl}`get-pane-info` for standard metadata. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "display_message", + "arguments": { + "format_string": "zoomed=#{window_zoomed_flag} dead=#{pane_dead}", + "pane_id": "%0" + } +} +``` + +Response (string): + +```text +zoomed=0 dead=0 +``` + +```{fastmcp-tool-input} pane_tools.display_message +``` diff --git a/docs/tools/enter-copy-mode.md b/docs/tools/enter-copy-mode.md new file mode 100644 index 0000000..a720e35 --- /dev/null +++ b/docs/tools/enter-copy-mode.md @@ -0,0 +1,44 @@ +```{fastmcp-tool} pane_tools.enter_copy_mode +``` + +**Use when** you need to scroll through scrollback history in a pane. +Optionally scroll up immediately after entering. Use +{tooliconl}`snapshot-pane` afterward to read the `scroll_position` and +visible content. + +**Side effects:** Puts the pane into copy mode. The pane stops receiving +new output until you exit copy mode. + +**Example:** + +```json +{ + "tool": "enter_copy_mode", + "arguments": { + "pane_id": "%0", + "scroll_up": 50 + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.enter_copy_mode +``` diff --git a/docs/tools/exit-copy-mode.md b/docs/tools/exit-copy-mode.md new file mode 100644 index 0000000..174dcd2 --- /dev/null +++ b/docs/tools/exit-copy-mode.md @@ -0,0 +1,40 @@ +```{fastmcp-tool} pane_tools.exit_copy_mode +``` + +**Use when** you're done scrolling through scrollback and want the pane to +resume receiving output. + +**Side effects:** Exits copy mode, returning the pane to normal. + +**Example:** + +```json +{ + "tool": "exit_copy_mode", + "arguments": { + "pane_id": "%0" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.exit_copy_mode +``` diff --git a/docs/tools/get-pane-info.md b/docs/tools/get-pane-info.md new file mode 100644 index 0000000..85ce43b --- /dev/null +++ b/docs/tools/get-pane-info.md @@ -0,0 +1,42 @@ +```{fastmcp-tool} pane_tools.get_pane_info +``` + +**Use when** you need pane dimensions, PID, current working directory, or +other metadata without reading the terminal content. + +**Avoid when** you need the actual text — use {tooliconl}`capture-pane`. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "get_pane_info", + "arguments": { + "pane_id": "%0" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.get_pane_info +``` diff --git a/docs/tools/get-server-info.md b/docs/tools/get-server-info.md new file mode 100644 index 0000000..2e1727d --- /dev/null +++ b/docs/tools/get-server-info.md @@ -0,0 +1,33 @@ +```{fastmcp-tool} server_tools.get_server_info +``` + +**Use when** you need to verify the tmux server is running, check its PID, +or inspect server-level state before creating sessions. + +**Avoid when** you only need session names — use {tooliconl}`list-sessions`. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "get_server_info", + "arguments": {} +} +``` + +Response: + +```json +{ + "is_alive": true, + "socket_name": null, + "socket_path": null, + "session_count": 2, + "version": "3.6a" +} +``` + +```{fastmcp-tool-input} server_tools.get_server_info +``` diff --git a/docs/tools/index.md b/docs/tools/index.md index 0a8e29f..8c3e1ec 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -300,8 +300,42 @@ Kill the entire tmux server. ```{toctree} :hidden: -sessions -windows -panes -options +capture-pane +clear-pane +create-session +create-window +display-message +enter-copy-mode +exit-copy-mode +get-pane-info +get-server-info +kill-pane +kill-server +kill-session +kill-window +list-panes +list-sessions +list-windows +move-window +paste-text +pipe-pane +rename-session +rename-window +resize-pane +resize-window +search-panes +select-layout +select-pane +select-window +send-keys +set-environment +set-option +set-pane-title +show-environment +show-option +snapshot-pane +split-window +swap-pane +wait-for-content-change +wait-for-text ``` diff --git a/docs/tools/kill-pane.md b/docs/tools/kill-pane.md new file mode 100644 index 0000000..7f6158b --- /dev/null +++ b/docs/tools/kill-pane.md @@ -0,0 +1,29 @@ +```{fastmcp-tool} pane_tools.kill_pane +``` + +**Use when** you're done with a specific terminal and want to remove it +without affecting sibling panes. + +**Avoid when** you want to remove the entire window — use {tooliconl}`kill-window`. + +**Side effects:** Destroys the pane. Not reversible. + +**Example:** + +```json +{ + "tool": "kill_pane", + "arguments": { + "pane_id": "%1" + } +} +``` + +Response (string): + +```text +Pane killed: %1 +``` + +```{fastmcp-tool-input} pane_tools.kill_pane +``` diff --git a/docs/tools/kill-server.md b/docs/tools/kill-server.md new file mode 100644 index 0000000..c4a0194 --- /dev/null +++ b/docs/tools/kill-server.md @@ -0,0 +1,27 @@ +```{fastmcp-tool} server_tools.kill_server +``` + +**Use when** you need to tear down the entire tmux server. This kills every +session, window, and pane. + +**Avoid when** you only need to remove one session — use {tooliconl}`kill-session`. + +**Side effects:** Destroys everything. Not reversible. + +**Example:** + +```json +{ + "tool": "kill_server", + "arguments": {} +} +``` + +Response (string): + +```text +Server killed successfully +``` + +```{fastmcp-tool-input} server_tools.kill_server +``` diff --git a/docs/tools/kill-session.md b/docs/tools/kill-session.md new file mode 100644 index 0000000..9f27f1e --- /dev/null +++ b/docs/tools/kill-session.md @@ -0,0 +1,29 @@ +```{fastmcp-tool} session_tools.kill_session +``` + +**Use when** you're done with a workspace and want to clean up. Kills all +windows and panes in the session. + +**Avoid when** you only want to close one window — use {tooliconl}`kill-window`. + +**Side effects:** Destroys the session and all its contents. Not reversible. + +**Example:** + +```json +{ + "tool": "kill_session", + "arguments": { + "session_name": "old-workspace" + } +} +``` + +Response (string): + +```text +Session killed: old-workspace +``` + +```{fastmcp-tool-input} session_tools.kill_session +``` diff --git a/docs/tools/kill-window.md b/docs/tools/kill-window.md new file mode 100644 index 0000000..1f84c80 --- /dev/null +++ b/docs/tools/kill-window.md @@ -0,0 +1,28 @@ +```{fastmcp-tool} window_tools.kill_window +``` + +**Use when** you're done with a window and all its panes. + +**Avoid when** you only want to remove one pane — use {tooliconl}`kill-pane`. + +**Side effects:** Destroys the window and all its panes. Not reversible. + +**Example:** + +```json +{ + "tool": "kill_window", + "arguments": { + "window_id": "@1" + } +} +``` + +Response (string): + +```text +Window killed: @1 +``` + +```{fastmcp-tool-input} window_tools.kill_window +``` diff --git a/docs/tools/list-panes.md b/docs/tools/list-panes.md new file mode 100644 index 0000000..a093ed4 --- /dev/null +++ b/docs/tools/list-panes.md @@ -0,0 +1,56 @@ +```{fastmcp-tool} window_tools.list_panes +``` + +**Use when** you need to discover which panes exist in a window before +sending keys or capturing output. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "list_panes", + "arguments": { + "session_name": "dev" + } +} +``` + +Response: + +```json +[ + { + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "15", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "build", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null + }, + { + "pane_id": "%1", + "pane_index": "1", + "pane_width": "80", + "pane_height": "8", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12400", + "pane_title": "", + "pane_active": "0", + "window_id": "@0", + "session_id": "$0", + "is_caller": null + } +] +``` + +```{fastmcp-tool-input} window_tools.list_panes +``` diff --git a/docs/tools/list-sessions.md b/docs/tools/list-sessions.md new file mode 100644 index 0000000..adca66e --- /dev/null +++ b/docs/tools/list-sessions.md @@ -0,0 +1,36 @@ +```{fastmcp-tool} server_tools.list_sessions +``` + +**Use when** you need session names, IDs, or attached status before deciding +which session to target. + +**Avoid when** you need window or pane details — use {tooliconl}`list-windows` or +{tooliconl}`list-panes` instead. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "list_sessions", + "arguments": {} +} +``` + +Response: + +```json +[ + { + "session_id": "$0", + "session_name": "myproject", + "window_count": 2, + "session_attached": "0", + "session_created": "1774521871" + } +] +``` + +```{fastmcp-tool-input} server_tools.list_sessions +``` diff --git a/docs/tools/list-windows.md b/docs/tools/list-windows.md new file mode 100644 index 0000000..240931b --- /dev/null +++ b/docs/tools/list-windows.md @@ -0,0 +1,54 @@ +```{fastmcp-tool} session_tools.list_windows +``` + +**Use when** you need window names, indices, or layout metadata within a +session before selecting a window to work with. + +**Avoid when** you need pane-level detail — use {tooliconl}`list-panes`. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "list_windows", + "arguments": { + "session_name": "dev" + } +} +``` + +Response: + +```json +[ + { + "window_id": "@0", + "window_name": "editor", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "c195,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", + "window_active": "1", + "window_width": "80", + "window_height": "24" + }, + { + "window_id": "@1", + "window_name": "server", + "window_index": "2", + "session_id": "$0", + "session_name": "dev", + "pane_count": 1, + "window_layout": "b25f,80x24,0,0,2", + "window_active": "0", + "window_width": "80", + "window_height": "24" + } +] +``` + +```{fastmcp-tool-input} session_tools.list_windows +``` diff --git a/docs/tools/move-window.md b/docs/tools/move-window.md new file mode 100644 index 0000000..db687e2 --- /dev/null +++ b/docs/tools/move-window.md @@ -0,0 +1,39 @@ +```{fastmcp-tool} window_tools.move_window +``` + +**Use when** you need to reorder windows within a session or move a window +to a different session entirely. + +**Side effects:** Changes the window's index or parent session. + +**Example:** + +```json +{ + "tool": "move_window", + "arguments": { + "window_id": "@1", + "destination_index": "1" + } +} +``` + +Response: + +```json +{ + "window_id": "@1", + "window_name": "server", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 1, + "window_layout": "b25f,80x24,0,0,2", + "window_active": "0", + "window_width": "80", + "window_height": "24" +} +``` + +```{fastmcp-tool-input} window_tools.move_window +``` diff --git a/docs/tools/options.md b/docs/tools/options.md deleted file mode 100644 index 5c3c1b8..0000000 --- a/docs/tools/options.md +++ /dev/null @@ -1,138 +0,0 @@ -# Options & Environment - -## Inspect - -```{fastmcp-tool} option_tools.show_option -``` - -**Use when** you need to check a tmux configuration value — buffer limits, -history size, status bar settings, etc. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "show_option", - "arguments": { - "option": "history-limit" - } -} -``` - -Response: - -```json -{ - "option": "history-limit", - "value": "2000" -} -``` - -```{fastmcp-tool-input} option_tools.show_option -``` - ---- - -```{fastmcp-tool} env_tools.show_environment -``` - -**Use when** you need to inspect tmux environment variables. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "show_environment", - "arguments": {} -} -``` - -Response: - -```json -{ - "variables": { - "SHELL": "/bin/zsh", - "TERM": "xterm-256color", - "HOME": "/home/user", - "USER": "user", - "LANG": "C.UTF-8" - } -} -``` - -```{fastmcp-tool-input} env_tools.show_environment -``` - -## Act - -```{fastmcp-tool} option_tools.set_option -``` - -**Use when** you need to change tmux behavior — adjusting history limits, -enabling mouse support, changing status bar format. - -**Side effects:** Changes the tmux option value. - -**Example:** - -```json -{ - "tool": "set_option", - "arguments": { - "option": "history-limit", - "value": "50000" - } -} -``` - -Response: - -```json -{ - "option": "history-limit", - "value": "50000", - "status": "set" -} -``` - -```{fastmcp-tool-input} option_tools.set_option -``` - ---- - -```{fastmcp-tool} env_tools.set_environment -``` - -**Use when** you need to set a tmux environment variable. - -**Side effects:** Sets the variable in the tmux server. - -**Example:** - -```json -{ - "tool": "set_environment", - "arguments": { - "name": "MY_VAR", - "value": "hello" - } -} -``` - -Response: - -```json -{ - "name": "MY_VAR", - "value": "hello", - "status": "set" -} -``` - -```{fastmcp-tool-input} env_tools.set_environment -``` diff --git a/docs/tools/panes.md b/docs/tools/panes.md deleted file mode 100644 index 73b3202..0000000 --- a/docs/tools/panes.md +++ /dev/null @@ -1,742 +0,0 @@ -# Panes - -## Inspect - -```{fastmcp-tool} pane_tools.capture_pane -``` - -**Use when** you need to read what's currently displayed in a terminal — -after running a command, checking output, or verifying state. - -**Avoid when** you need to search across multiple panes at once — use -{tooliconl}`search-panes`. If you only need pane metadata (not content), use -{tooliconl}`get-pane-info`. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "capture_pane", - "arguments": { - "pane_id": "%0", - "start": -50 - } -} -``` - -Response (string): - -```text -$ echo "Running tests..." -Running tests... -$ echo "PASS: test_auth (0.3s)" -PASS: test_auth (0.3s) -$ echo "FAIL: test_upload (AssertionError)" -FAIL: test_upload (AssertionError) -$ echo "3 tests: 2 passed, 1 failed" -3 tests: 2 passed, 1 failed -$ -``` - -```{fastmcp-tool-input} pane_tools.capture_pane -``` - ---- - -```{fastmcp-tool} pane_tools.get_pane_info -``` - -**Use when** you need pane dimensions, PID, current working directory, or -other metadata without reading the terminal content. - -**Avoid when** you need the actual text — use {tooliconl}`capture-pane`. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "get_pane_info", - "arguments": { - "pane_id": "%0" - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "pane_index": "0", - "pane_width": "80", - "pane_height": "24", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.get_pane_info -``` - ---- - -```{fastmcp-tool} pane_tools.search_panes -``` - -**Use when** you need to find specific text across multiple panes — locating -which pane has an error, finding a running process, or checking output -without knowing which pane to look in. - -**Avoid when** you already know the target pane — use {tooliconl}`capture-pane` -directly. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "search_panes", - "arguments": { - "pattern": "FAIL", - "session_name": "dev" - } -} -``` - -Response: - -```json -[ - { - "pane_id": "%0", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "window_id": "@0", - "window_name": "editor", - "session_id": "$0", - "session_name": "dev", - "matched_lines": [ - "FAIL: test_upload (AssertionError)", - "3 tests: 2 passed, 1 failed" - ], - "is_caller": null - } -] -``` - -```{fastmcp-tool-input} pane_tools.search_panes -``` - ---- - -```{fastmcp-tool} pane_tools.wait_for_text -``` - -**Use when** you need to block until specific output appears — waiting for a -server to start, a build to complete, or a prompt to return. - -**Avoid when** the expected text may never appear — always set a reasonable -`timeout`. For known output, {tooliconl}`capture-pane` after a known delay -may suffice, but `wait_for_text` is preferred because it adapts to variable -timing. - -**Side effects:** None. Readonly. Blocks until text appears or timeout. - -**Example:** - -```json -{ - "tool": "wait_for_text", - "arguments": { - "pattern": "Server listening", - "pane_id": "%2", - "timeout": 30 - } -} -``` - -Response: - -```json -{ - "found": true, - "matched_lines": [ - "Server listening on port 8000" - ], - "pane_id": "%2", - "elapsed_seconds": 0.002, - "timed_out": false -} -``` - -```{fastmcp-tool-input} pane_tools.wait_for_text -``` - ---- - -```{fastmcp-tool} pane_tools.snapshot_pane -``` - -**Use when** you need a complete picture of a pane in a single call — visible -text plus cursor position, whether the pane is in copy mode, scroll offset, -and scrollback history size. Replaces separate `capture_pane` + -`get_pane_info` calls when you need to reason about cursor location or -terminal mode. - -**Avoid when** you only need raw text — {tooliconl}`capture-pane` is lighter. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "snapshot_pane", - "arguments": { - "pane_id": "%0" - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "content": "$ npm test\n\nPASS src/auth.test.ts\nTests: 3 passed\n$", - "cursor_x": 2, - "cursor_y": 4, - "pane_width": 80, - "pane_height": 24, - "pane_in_mode": false, - "pane_mode": null, - "scroll_position": null, - "history_size": 142, - "title": "", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.snapshot_pane -``` - ---- - -```{fastmcp-tool} pane_tools.wait_for_content_change -``` - -**Use when** you've sent a command and need to wait for *something* to happen, -but you don't know what the output will look like. Unlike -{tooliconl}`wait-for-text`, this waits for *any* screen change rather than a -specific pattern. - -**Avoid when** you know the expected output — {tooliconl}`wait-for-text` is more -precise and avoids false positives from unrelated output. - -**Side effects:** None. Readonly. Blocks until content changes or timeout. - -**Example:** - -```json -{ - "tool": "wait_for_content_change", - "arguments": { - "pane_id": "%0", - "timeout": 10 - } -} -``` - -Response: - -```json -{ - "changed": true, - "pane_id": "%0", - "elapsed_seconds": 1.234, - "timed_out": false -} -``` - -```{fastmcp-tool-input} pane_tools.wait_for_content_change -``` - ---- - -```{fastmcp-tool} pane_tools.display_message -``` - -**Use when** you need to query arbitrary tmux variables — zoom state, pane -dead flag, client activity, or any `#{format}` string that isn't covered by -other tools. - -**Avoid when** a dedicated tool already provides the information — e.g. use -{tooliconl}`snapshot-pane` for cursor position and mode, or -{tooliconl}`get-pane-info` for standard metadata. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "display_message", - "arguments": { - "format_string": "zoomed=#{window_zoomed_flag} dead=#{pane_dead}", - "pane_id": "%0" - } -} -``` - -Response (string): - -```text -zoomed=0 dead=0 -``` - -```{fastmcp-tool-input} pane_tools.display_message -``` - -## Act - -```{fastmcp-tool} pane_tools.send_keys -``` - -**Use when** you need to type commands, press keys, or interact with a -terminal. This is the primary way to execute commands in tmux panes. - -**Avoid when** you need to run something and immediately capture the result — -send keys first, then use {tooliconl}`capture-pane` or {tooliconl}`wait-for-text`. - -**Side effects:** Sends keystrokes to the pane. If `enter` is true (default), -the command executes. - -**Example:** - -```json -{ - "tool": "send_keys", - "arguments": { - "keys": "npm start", - "pane_id": "%2" - } -} -``` - -Response (string): - -```text -Keys sent to pane %2 -``` - -```{fastmcp-tool-input} pane_tools.send_keys -``` - ---- - -```{fastmcp-tool} pane_tools.set_pane_title -``` - -**Use when** you want to label a pane for identification. - -**Side effects:** Changes the pane title. - -**Example:** - -```json -{ - "tool": "set_pane_title", - "arguments": { - "pane_id": "%0", - "title": "build" - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "pane_index": "0", - "pane_width": "80", - "pane_height": "24", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "build", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.set_pane_title -``` - ---- - -```{fastmcp-tool} pane_tools.clear_pane -``` - -**Use when** you want a clean terminal before capturing output. - -**Side effects:** Clears the pane's visible content. - -**Example:** - -```json -{ - "tool": "clear_pane", - "arguments": { - "pane_id": "%0" - } -} -``` - -Response (string): - -```text -Pane cleared: %0 -``` - -```{fastmcp-tool-input} pane_tools.clear_pane -``` - ---- - -```{fastmcp-tool} pane_tools.resize_pane -``` - -**Use when** you need to adjust pane dimensions. - -**Side effects:** Changes pane size. May affect adjacent panes. - -**Example:** - -```json -{ - "tool": "resize_pane", - "arguments": { - "pane_id": "%0", - "height": 15 - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "pane_index": "0", - "pane_width": "80", - "pane_height": "15", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.resize_pane -``` - ---- - -```{fastmcp-tool} pane_tools.select_pane -``` - -**Use when** you need to focus a specific pane — by ID for a known target, -or by direction (`up`, `down`, `left`, `right`, `last`, `next`, `previous`) -to navigate a multi-pane layout. - -**Side effects:** Changes the active pane in the window. - -**Example:** - -```json -{ - "tool": "select_pane", - "arguments": { - "direction": "down", - "window_id": "@0" - } -} -``` - -Response: - -```json -{ - "pane_id": "%1", - "pane_index": "1", - "pane_width": "80", - "pane_height": "11", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12400", - "pane_title": "", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.select_pane -``` - ---- - -```{fastmcp-tool} pane_tools.swap_pane -``` - -**Use when** you want to rearrange pane positions without changing content — -e.g. moving a log pane from bottom to top. - -**Side effects:** Exchanges the visual positions of two panes. - -**Example:** - -```json -{ - "tool": "swap_pane", - "arguments": { - "source_pane_id": "%0", - "target_pane_id": "%1" - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "pane_index": "1", - "pane_width": "80", - "pane_height": "11", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.swap_pane -``` - ---- - -```{fastmcp-tool} pane_tools.pipe_pane -``` - -**Use when** you need to log pane output to a file — useful for monitoring -long-running processes or capturing output that scrolls past the visible -area. - -**Avoid when** you only need a one-time capture — use {tooliconl}`capture-pane` -with `start`/`end` to read scrollback. - -**Side effects:** Starts or stops piping output to a file. Call with -`output_path=null` to stop. - -**Example:** - -```json -{ - "tool": "pipe_pane", - "arguments": { - "pane_id": "%0", - "output_path": "/tmp/build.log" - } -} -``` - -Response (string): - -```text -Piping pane %0 to /tmp/build.log -``` - -```{fastmcp-tool-input} pane_tools.pipe_pane -``` - ---- - -```{fastmcp-tool} pane_tools.enter_copy_mode -``` - -**Use when** you need to scroll through scrollback history in a pane. -Optionally scroll up immediately after entering. Use -{tooliconl}`snapshot-pane` afterward to read the `scroll_position` and -visible content. - -**Side effects:** Puts the pane into copy mode. The pane stops receiving -new output until you exit copy mode. - -**Example:** - -```json -{ - "tool": "enter_copy_mode", - "arguments": { - "pane_id": "%0", - "scroll_up": 50 - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "pane_index": "0", - "pane_width": "80", - "pane_height": "24", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.enter_copy_mode -``` - ---- - -```{fastmcp-tool} pane_tools.exit_copy_mode -``` - -**Use when** you're done scrolling through scrollback and want the pane to -resume receiving output. - -**Side effects:** Exits copy mode, returning the pane to normal. - -**Example:** - -```json -{ - "tool": "exit_copy_mode", - "arguments": { - "pane_id": "%0" - } -} -``` - -Response: - -```json -{ - "pane_id": "%0", - "pane_index": "0", - "pane_width": "80", - "pane_height": "24", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} pane_tools.exit_copy_mode -``` - ---- - -```{fastmcp-tool} pane_tools.paste_text -``` - -**Use when** you need to paste multi-line text into a pane — e.g. a code -block, a config snippet, or a heredoc. Uses tmux paste buffers for clean -multi-line input instead of sending text line-by-line via -{tooliconl}`send-keys`. - -**Side effects:** Pastes text into the pane. With `bracket=true` (default), -uses bracketed paste mode so the terminal knows this is pasted text. - -**Example:** - -```json -{ - "tool": "paste_text", - "arguments": { - "text": "def hello():\n print('world')\n", - "pane_id": "%0" - } -} -``` - -Response (string): - -```text -Text pasted to pane %0 -``` - -```{fastmcp-tool-input} pane_tools.paste_text -``` - -## Destroy - -```{fastmcp-tool} pane_tools.kill_pane -``` - -**Use when** you're done with a specific terminal and want to remove it -without affecting sibling panes. - -**Avoid when** you want to remove the entire window — use {tooliconl}`kill-window`. - -**Side effects:** Destroys the pane. Not reversible. - -**Example:** - -```json -{ - "tool": "kill_pane", - "arguments": { - "pane_id": "%1" - } -} -``` - -Response (string): - -```text -Pane killed: %1 -``` - -```{fastmcp-tool-input} pane_tools.kill_pane -``` diff --git a/docs/tools/paste-text.md b/docs/tools/paste-text.md new file mode 100644 index 0000000..aa974bc --- /dev/null +++ b/docs/tools/paste-text.md @@ -0,0 +1,31 @@ +```{fastmcp-tool} pane_tools.paste_text +``` + +**Use when** you need to paste multi-line text into a pane — e.g. a code +block, a config snippet, or a heredoc. Uses tmux paste buffers for clean +multi-line input instead of sending text line-by-line via +{tooliconl}`send-keys`. + +**Side effects:** Pastes text into the pane. With `bracket=true` (default), +uses bracketed paste mode so the terminal knows this is pasted text. + +**Example:** + +```json +{ + "tool": "paste_text", + "arguments": { + "text": "def hello():\n print('world')\n", + "pane_id": "%0" + } +} +``` + +Response (string): + +```text +Text pasted to pane %0 +``` + +```{fastmcp-tool-input} pane_tools.paste_text +``` diff --git a/docs/tools/pipe-pane.md b/docs/tools/pipe-pane.md new file mode 100644 index 0000000..48f4261 --- /dev/null +++ b/docs/tools/pipe-pane.md @@ -0,0 +1,33 @@ +```{fastmcp-tool} pane_tools.pipe_pane +``` + +**Use when** you need to log pane output to a file — useful for monitoring +long-running processes or capturing output that scrolls past the visible +area. + +**Avoid when** you only need a one-time capture — use {tooliconl}`capture-pane` +with `start`/`end` to read scrollback. + +**Side effects:** Starts or stops piping output to a file. Call with +`output_path=null` to stop. + +**Example:** + +```json +{ + "tool": "pipe_pane", + "arguments": { + "pane_id": "%0", + "output_path": "/tmp/build.log" + } +} +``` + +Response (string): + +```text +Piping pane %0 to /tmp/build.log +``` + +```{fastmcp-tool-input} pane_tools.pipe_pane +``` diff --git a/docs/tools/rename-session.md b/docs/tools/rename-session.md new file mode 100644 index 0000000..b335a39 --- /dev/null +++ b/docs/tools/rename-session.md @@ -0,0 +1,33 @@ +```{fastmcp-tool} session_tools.rename_session +``` + +**Use when** a session name no longer reflects its purpose. + +**Side effects:** Renames the session. Existing references by old name will break. + +**Example:** + +```json +{ + "tool": "rename_session", + "arguments": { + "session_name": "old-name", + "new_name": "new-name" + } +} +``` + +Response: + +```json +{ + "session_id": "$0", + "session_name": "new-name", + "window_count": 2, + "session_attached": "0", + "session_created": "1774521871" +} +``` + +```{fastmcp-tool-input} session_tools.rename_session +``` diff --git a/docs/tools/rename-window.md b/docs/tools/rename-window.md new file mode 100644 index 0000000..c2c8c6d --- /dev/null +++ b/docs/tools/rename-window.md @@ -0,0 +1,38 @@ +```{fastmcp-tool} window_tools.rename_window +``` + +**Use when** a window name no longer reflects its purpose. + +**Side effects:** Renames the window. + +**Example:** + +```json +{ + "tool": "rename_window", + "arguments": { + "session_name": "dev", + "new_name": "build" + } +} +``` + +Response: + +```json +{ + "window_id": "@0", + "window_name": "build", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]", + "window_active": "1", + "window_width": "80", + "window_height": "24" +} +``` + +```{fastmcp-tool-input} window_tools.rename_window +``` diff --git a/docs/tools/resize-pane.md b/docs/tools/resize-pane.md new file mode 100644 index 0000000..2cc7fcd --- /dev/null +++ b/docs/tools/resize-pane.md @@ -0,0 +1,40 @@ +```{fastmcp-tool} pane_tools.resize_pane +``` + +**Use when** you need to adjust pane dimensions. + +**Side effects:** Changes pane size. May affect adjacent panes. + +**Example:** + +```json +{ + "tool": "resize_pane", + "arguments": { + "pane_id": "%0", + "height": 15 + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "15", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.resize_pane +``` diff --git a/docs/tools/resize-window.md b/docs/tools/resize-window.md new file mode 100644 index 0000000..b59ca70 --- /dev/null +++ b/docs/tools/resize-window.md @@ -0,0 +1,39 @@ +```{fastmcp-tool} window_tools.resize_window +``` + +**Use when** you need to adjust the window dimensions. + +**Side effects:** Changes window size. + +**Example:** + +```json +{ + "tool": "resize_window", + "arguments": { + "session_name": "dev", + "width": 120, + "height": 40 + } +} +``` + +Response: + +```json +{ + "window_id": "@0", + "window_name": "editor", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "baaa,120x40,0,0[120x20,0,0,0,120x19,0,21,1]", + "window_active": "1", + "window_width": "120", + "window_height": "40" +} +``` + +```{fastmcp-tool-input} window_tools.resize_window +``` diff --git a/docs/tools/search-panes.md b/docs/tools/search-panes.md new file mode 100644 index 0000000..6dd8f16 --- /dev/null +++ b/docs/tools/search-panes.md @@ -0,0 +1,47 @@ +```{fastmcp-tool} pane_tools.search_panes +``` + +**Use when** you need to find specific text across multiple panes — locating +which pane has an error, finding a running process, or checking output +without knowing which pane to look in. + +**Avoid when** you already know the target pane — use {tooliconl}`capture-pane` +directly. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "search_panes", + "arguments": { + "pattern": "FAIL", + "session_name": "dev" + } +} +``` + +Response: + +```json +[ + { + "pane_id": "%0", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "window_id": "@0", + "window_name": "editor", + "session_id": "$0", + "session_name": "dev", + "matched_lines": [ + "FAIL: test_upload (AssertionError)", + "3 tests: 2 passed, 1 failed" + ], + "is_caller": null + } +] +``` + +```{fastmcp-tool-input} pane_tools.search_panes +``` diff --git a/docs/tools/select-layout.md b/docs/tools/select-layout.md new file mode 100644 index 0000000..8a2146f --- /dev/null +++ b/docs/tools/select-layout.md @@ -0,0 +1,39 @@ +```{fastmcp-tool} window_tools.select_layout +``` + +**Use when** you want to rearrange panes — `even-horizontal`, +`even-vertical`, `main-horizontal`, `main-vertical`, or `tiled`. + +**Side effects:** Rearranges all panes in the window. + +**Example:** + +```json +{ + "tool": "select_layout", + "arguments": { + "session_name": "dev", + "layout": "even-vertical" + } +} +``` + +Response: + +```json +{ + "window_id": "@0", + "window_name": "editor", + "window_index": "1", + "session_id": "$0", + "session_name": "dev", + "pane_count": 2, + "window_layout": "even-vertical,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", + "window_active": "1", + "window_width": "80", + "window_height": "24" +} +``` + +```{fastmcp-tool-input} window_tools.select_layout +``` diff --git a/docs/tools/select-pane.md b/docs/tools/select-pane.md new file mode 100644 index 0000000..7495aa6 --- /dev/null +++ b/docs/tools/select-pane.md @@ -0,0 +1,42 @@ +```{fastmcp-tool} pane_tools.select_pane +``` + +**Use when** you need to focus a specific pane — by ID for a known target, +or by direction (`up`, `down`, `left`, `right`, `last`, `next`, `previous`) +to navigate a multi-pane layout. + +**Side effects:** Changes the active pane in the window. + +**Example:** + +```json +{ + "tool": "select_pane", + "arguments": { + "direction": "down", + "window_id": "@0" + } +} +``` + +Response: + +```json +{ + "pane_id": "%1", + "pane_index": "1", + "pane_width": "80", + "pane_height": "11", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12400", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.select_pane +``` diff --git a/docs/tools/select-window.md b/docs/tools/select-window.md new file mode 100644 index 0000000..0ebe289 --- /dev/null +++ b/docs/tools/select-window.md @@ -0,0 +1,39 @@ +```{fastmcp-tool} session_tools.select_window +``` + +**Use when** you need to switch focus to a different window — by ID, index, +or direction (`next`, `previous`, `last`). + +**Side effects:** Changes the active window in the session. + +**Example:** + +```json +{ + "tool": "select_window", + "arguments": { + "direction": "next", + "session_name": "dev" + } +} +``` + +Response: + +```json +{ + "window_id": "@1", + "window_name": "server", + "window_index": "2", + "session_id": "$0", + "session_name": "dev", + "pane_count": 1, + "window_layout": "b25f,80x24,0,0,2", + "window_active": "1", + "window_width": "80", + "window_height": "24" +} +``` + +```{fastmcp-tool-input} session_tools.select_window +``` diff --git a/docs/tools/send-keys.md b/docs/tools/send-keys.md new file mode 100644 index 0000000..4bdaa75 --- /dev/null +++ b/docs/tools/send-keys.md @@ -0,0 +1,32 @@ +```{fastmcp-tool} pane_tools.send_keys +``` + +**Use when** you need to type commands, press keys, or interact with a +terminal. This is the primary way to execute commands in tmux panes. + +**Avoid when** you need to run something and immediately capture the result — +send keys first, then use {tooliconl}`capture-pane` or {tooliconl}`wait-for-text`. + +**Side effects:** Sends keystrokes to the pane. If `enter` is true (default), +the command executes. + +**Example:** + +```json +{ + "tool": "send_keys", + "arguments": { + "keys": "npm start", + "pane_id": "%2" + } +} +``` + +Response (string): + +```text +Keys sent to pane %2 +``` + +```{fastmcp-tool-input} pane_tools.send_keys +``` diff --git a/docs/tools/sessions.md b/docs/tools/sessions.md deleted file mode 100644 index 984f042..0000000 --- a/docs/tools/sessions.md +++ /dev/null @@ -1,255 +0,0 @@ -# Sessions - -## Inspect - -```{fastmcp-tool} server_tools.list_sessions -``` - -**Use when** you need session names, IDs, or attached status before deciding -which session to target. - -**Avoid when** you need window or pane details — use {tooliconl}`list-windows` or -{tooliconl}`list-panes` instead. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "list_sessions", - "arguments": {} -} -``` - -Response: - -```json -[ - { - "session_id": "$0", - "session_name": "myproject", - "window_count": 2, - "session_attached": "0", - "session_created": "1774521871" - } -] -``` - -```{fastmcp-tool-input} server_tools.list_sessions -``` - ---- - -```{fastmcp-tool} server_tools.get_server_info -``` - -**Use when** you need to verify the tmux server is running, check its PID, -or inspect server-level state before creating sessions. - -**Avoid when** you only need session names — use {tooliconl}`list-sessions`. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "get_server_info", - "arguments": {} -} -``` - -Response: - -```json -{ - "is_alive": true, - "socket_name": null, - "socket_path": null, - "session_count": 2, - "version": "3.6a" -} -``` - -```{fastmcp-tool-input} server_tools.get_server_info -``` - -## Act - -```{fastmcp-tool} server_tools.create_session -``` - -**Use when** you need a new isolated workspace. Sessions are the top-level -container — create one before creating windows or panes. - -**Avoid when** a session with the target name already exists — check with -{tooliconl}`list-sessions` first, or the command will fail. - -**Side effects:** Creates a new tmux session with one window and one pane. - -**Example:** - -```json -{ - "tool": "create_session", - "arguments": { - "session_name": "dev" - } -} -``` - -Response: - -```json -{ - "session_id": "$1", - "session_name": "dev", - "window_count": 1, - "session_attached": "0", - "session_created": "1774521872" -} -``` - -```{fastmcp-tool-input} server_tools.create_session -``` - ---- - -```{fastmcp-tool} session_tools.rename_session -``` - -**Use when** a session name no longer reflects its purpose. - -**Side effects:** Renames the session. Existing references by old name will break. - -**Example:** - -```json -{ - "tool": "rename_session", - "arguments": { - "session_name": "old-name", - "new_name": "new-name" - } -} -``` - -Response: - -```json -{ - "session_id": "$0", - "session_name": "new-name", - "window_count": 2, - "session_attached": "0", - "session_created": "1774521871" -} -``` - -```{fastmcp-tool-input} session_tools.rename_session -``` - ---- - -```{fastmcp-tool} session_tools.select_window -``` - -**Use when** you need to switch focus to a different window — by ID, index, -or direction (`next`, `previous`, `last`). - -**Side effects:** Changes the active window in the session. - -**Example:** - -```json -{ - "tool": "select_window", - "arguments": { - "direction": "next", - "session_name": "dev" - } -} -``` - -Response: - -```json -{ - "window_id": "@1", - "window_name": "server", - "window_index": "2", - "session_id": "$0", - "session_name": "dev", - "pane_count": 1, - "window_layout": "b25f,80x24,0,0,2", - "window_active": "1", - "window_width": "80", - "window_height": "24" -} -``` - -```{fastmcp-tool-input} session_tools.select_window -``` - -## Destroy - -```{fastmcp-tool} session_tools.kill_session -``` - -**Use when** you're done with a workspace and want to clean up. Kills all -windows and panes in the session. - -**Avoid when** you only want to close one window — use {tooliconl}`kill-window`. - -**Side effects:** Destroys the session and all its contents. Not reversible. - -**Example:** - -```json -{ - "tool": "kill_session", - "arguments": { - "session_name": "old-workspace" - } -} -``` - -Response (string): - -```text -Session killed: old-workspace -``` - -```{fastmcp-tool-input} session_tools.kill_session -``` - ---- - -```{fastmcp-tool} server_tools.kill_server -``` - -**Use when** you need to tear down the entire tmux server. This kills every -session, window, and pane. - -**Avoid when** you only need to remove one session — use {tooliconl}`kill-session`. - -**Side effects:** Destroys everything. Not reversible. - -**Example:** - -```json -{ - "tool": "kill_server", - "arguments": {} -} -``` - -Response (string): - -```text -Server killed successfully -``` - -```{fastmcp-tool-input} server_tools.kill_server -``` diff --git a/docs/tools/set-environment.md b/docs/tools/set-environment.md new file mode 100644 index 0000000..4d50c8a --- /dev/null +++ b/docs/tools/set-environment.md @@ -0,0 +1,31 @@ +```{fastmcp-tool} env_tools.set_environment +``` + +**Use when** you need to set a tmux environment variable. + +**Side effects:** Sets the variable in the tmux server. + +**Example:** + +```json +{ + "tool": "set_environment", + "arguments": { + "name": "MY_VAR", + "value": "hello" + } +} +``` + +Response: + +```json +{ + "name": "MY_VAR", + "value": "hello", + "status": "set" +} +``` + +```{fastmcp-tool-input} env_tools.set_environment +``` diff --git a/docs/tools/set-option.md b/docs/tools/set-option.md new file mode 100644 index 0000000..a27d5be --- /dev/null +++ b/docs/tools/set-option.md @@ -0,0 +1,32 @@ +```{fastmcp-tool} option_tools.set_option +``` + +**Use when** you need to change tmux behavior — adjusting history limits, +enabling mouse support, changing status bar format. + +**Side effects:** Changes the tmux option value. + +**Example:** + +```json +{ + "tool": "set_option", + "arguments": { + "option": "history-limit", + "value": "50000" + } +} +``` + +Response: + +```json +{ + "option": "history-limit", + "value": "50000", + "status": "set" +} +``` + +```{fastmcp-tool-input} option_tools.set_option +``` diff --git a/docs/tools/set-pane-title.md b/docs/tools/set-pane-title.md new file mode 100644 index 0000000..65ec2d8 --- /dev/null +++ b/docs/tools/set-pane-title.md @@ -0,0 +1,40 @@ +```{fastmcp-tool} pane_tools.set_pane_title +``` + +**Use when** you want to label a pane for identification. + +**Side effects:** Changes the pane title. + +**Example:** + +```json +{ + "tool": "set_pane_title", + "arguments": { + "pane_id": "%0", + "title": "build" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "0", + "pane_width": "80", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "build", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.set_pane_title +``` diff --git a/docs/tools/show-environment.md b/docs/tools/show-environment.md new file mode 100644 index 0000000..66171ce --- /dev/null +++ b/docs/tools/show-environment.md @@ -0,0 +1,32 @@ +```{fastmcp-tool} env_tools.show_environment +``` + +**Use when** you need to inspect tmux environment variables. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "show_environment", + "arguments": {} +} +``` + +Response: + +```json +{ + "variables": { + "SHELL": "/bin/zsh", + "TERM": "xterm-256color", + "HOME": "/home/user", + "USER": "user", + "LANG": "C.UTF-8" + } +} +``` + +```{fastmcp-tool-input} env_tools.show_environment +``` diff --git a/docs/tools/show-option.md b/docs/tools/show-option.md new file mode 100644 index 0000000..4aa597a --- /dev/null +++ b/docs/tools/show-option.md @@ -0,0 +1,30 @@ +```{fastmcp-tool} option_tools.show_option +``` + +**Use when** you need to check a tmux configuration value — buffer limits, +history size, status bar settings, etc. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "show_option", + "arguments": { + "option": "history-limit" + } +} +``` + +Response: + +```json +{ + "option": "history-limit", + "value": "2000" +} +``` + +```{fastmcp-tool-input} option_tools.show_option +``` diff --git a/docs/tools/snapshot-pane.md b/docs/tools/snapshot-pane.md new file mode 100644 index 0000000..3ee2747 --- /dev/null +++ b/docs/tools/snapshot-pane.md @@ -0,0 +1,47 @@ +```{fastmcp-tool} pane_tools.snapshot_pane +``` + +**Use when** you need a complete picture of a pane in a single call — visible +text plus cursor position, whether the pane is in copy mode, scroll offset, +and scrollback history size. Replaces separate `capture_pane` + +`get_pane_info` calls when you need to reason about cursor location or +terminal mode. + +**Avoid when** you only need raw text — {tooliconl}`capture-pane` is lighter. + +**Side effects:** None. Readonly. + +**Example:** + +```json +{ + "tool": "snapshot_pane", + "arguments": { + "pane_id": "%0" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "content": "$ npm test\n\nPASS src/auth.test.ts\nTests: 3 passed\n$", + "cursor_x": 2, + "cursor_y": 4, + "pane_width": 80, + "pane_height": 24, + "pane_in_mode": false, + "pane_mode": null, + "scroll_position": null, + "history_size": 142, + "title": "", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.snapshot_pane +``` diff --git a/docs/tools/split-window.md b/docs/tools/split-window.md new file mode 100644 index 0000000..07acdec --- /dev/null +++ b/docs/tools/split-window.md @@ -0,0 +1,41 @@ +```{fastmcp-tool} window_tools.split_window +``` + +**Use when** you need side-by-side or stacked terminals within the same +window. + +**Side effects:** Creates a new pane by splitting an existing one. + +**Example:** + +```json +{ + "tool": "split_window", + "arguments": { + "session_name": "dev", + "direction": "right" + } +} +``` + +Response: + +```json +{ + "pane_id": "%4", + "pane_index": "1", + "pane_width": "39", + "pane_height": "24", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "3732", + "pane_title": "", + "pane_active": "0", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} window_tools.split_window +``` diff --git a/docs/tools/swap-pane.md b/docs/tools/swap-pane.md new file mode 100644 index 0000000..d179952 --- /dev/null +++ b/docs/tools/swap-pane.md @@ -0,0 +1,41 @@ +```{fastmcp-tool} pane_tools.swap_pane +``` + +**Use when** you want to rearrange pane positions without changing content — +e.g. moving a log pane from bottom to top. + +**Side effects:** Exchanges the visual positions of two panes. + +**Example:** + +```json +{ + "tool": "swap_pane", + "arguments": { + "source_pane_id": "%0", + "target_pane_id": "%1" + } +} +``` + +Response: + +```json +{ + "pane_id": "%0", + "pane_index": "1", + "pane_width": "80", + "pane_height": "11", + "pane_current_command": "zsh", + "pane_current_path": "/home/user/myproject", + "pane_pid": "12345", + "pane_title": "", + "pane_active": "1", + "window_id": "@0", + "session_id": "$0", + "is_caller": null +} +``` + +```{fastmcp-tool-input} pane_tools.swap_pane +``` diff --git a/docs/tools/wait-for-content-change.md b/docs/tools/wait-for-content-change.md new file mode 100644 index 0000000..da84111 --- /dev/null +++ b/docs/tools/wait-for-content-change.md @@ -0,0 +1,38 @@ +```{fastmcp-tool} pane_tools.wait_for_content_change +``` + +**Use when** you've sent a command and need to wait for *something* to happen, +but you don't know what the output will look like. Unlike +{tooliconl}`wait-for-text`, this waits for *any* screen change rather than a +specific pattern. + +**Avoid when** you know the expected output — {tooliconl}`wait-for-text` is more +precise and avoids false positives from unrelated output. + +**Side effects:** None. Readonly. Blocks until content changes or timeout. + +**Example:** + +```json +{ + "tool": "wait_for_content_change", + "arguments": { + "pane_id": "%0", + "timeout": 10 + } +} +``` + +Response: + +```json +{ + "changed": true, + "pane_id": "%0", + "elapsed_seconds": 1.234, + "timed_out": false +} +``` + +```{fastmcp-tool-input} pane_tools.wait_for_content_change +``` diff --git a/docs/tools/wait-for-text.md b/docs/tools/wait-for-text.md new file mode 100644 index 0000000..91aaaf6 --- /dev/null +++ b/docs/tools/wait-for-text.md @@ -0,0 +1,42 @@ +```{fastmcp-tool} pane_tools.wait_for_text +``` + +**Use when** you need to block until specific output appears — waiting for a +server to start, a build to complete, or a prompt to return. + +**Avoid when** the expected text may never appear — always set a reasonable +`timeout`. For known output, {tooliconl}`capture-pane` after a known delay +may suffice, but `wait_for_text` is preferred because it adapts to variable +timing. + +**Side effects:** None. Readonly. Blocks until text appears or timeout. + +**Example:** + +```json +{ + "tool": "wait_for_text", + "arguments": { + "pattern": "Server listening", + "pane_id": "%2", + "timeout": 30 + } +} +``` + +Response: + +```json +{ + "found": true, + "matched_lines": [ + "Server listening on port 8000" + ], + "pane_id": "%2", + "elapsed_seconds": 0.002, + "timed_out": false +} +``` + +```{fastmcp-tool-input} pane_tools.wait_for_text +``` diff --git a/docs/tools/windows.md b/docs/tools/windows.md deleted file mode 100644 index 91684c0..0000000 --- a/docs/tools/windows.md +++ /dev/null @@ -1,400 +0,0 @@ -# Windows - -## Inspect - -```{fastmcp-tool} session_tools.list_windows -``` - -**Use when** you need window names, indices, or layout metadata within a -session before selecting a window to work with. - -**Avoid when** you need pane-level detail — use {tooliconl}`list-panes`. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "list_windows", - "arguments": { - "session_name": "dev" - } -} -``` - -Response: - -```json -[ - { - "window_id": "@0", - "window_name": "editor", - "window_index": "1", - "session_id": "$0", - "session_name": "dev", - "pane_count": 2, - "window_layout": "c195,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", - "window_active": "1", - "window_width": "80", - "window_height": "24" - }, - { - "window_id": "@1", - "window_name": "server", - "window_index": "2", - "session_id": "$0", - "session_name": "dev", - "pane_count": 1, - "window_layout": "b25f,80x24,0,0,2", - "window_active": "0", - "window_width": "80", - "window_height": "24" - } -] -``` - -```{fastmcp-tool-input} session_tools.list_windows -``` - ---- - -```{fastmcp-tool} window_tools.list_panes -``` - -**Use when** you need to discover which panes exist in a window before -sending keys or capturing output. - -**Side effects:** None. Readonly. - -**Example:** - -```json -{ - "tool": "list_panes", - "arguments": { - "session_name": "dev" - } -} -``` - -Response: - -```json -[ - { - "pane_id": "%0", - "pane_index": "0", - "pane_width": "80", - "pane_height": "15", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12345", - "pane_title": "build", - "pane_active": "1", - "window_id": "@0", - "session_id": "$0", - "is_caller": null - }, - { - "pane_id": "%1", - "pane_index": "1", - "pane_width": "80", - "pane_height": "8", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "12400", - "pane_title": "", - "pane_active": "0", - "window_id": "@0", - "session_id": "$0", - "is_caller": null - } -] -``` - -```{fastmcp-tool-input} window_tools.list_panes -``` - -## Act - -```{fastmcp-tool} session_tools.create_window -``` - -**Use when** you need a new terminal workspace within an existing session. - -**Side effects:** Creates a new window. Attaches to it if `attach` is true. - -**Example:** - -```json -{ - "tool": "create_window", - "arguments": { - "session_name": "dev", - "window_name": "logs" - } -} -``` - -Response: - -```json -{ - "window_id": "@2", - "window_name": "logs", - "window_index": "3", - "session_id": "$0", - "session_name": "dev", - "pane_count": 1, - "window_layout": "b25f,80x24,0,0,5", - "window_active": "1", - "window_width": "80", - "window_height": "24" -} -``` - -```{fastmcp-tool-input} session_tools.create_window -``` - ---- - -```{fastmcp-tool} window_tools.split_window -``` - -**Use when** you need side-by-side or stacked terminals within the same -window. - -**Side effects:** Creates a new pane by splitting an existing one. - -**Example:** - -```json -{ - "tool": "split_window", - "arguments": { - "session_name": "dev", - "direction": "right" - } -} -``` - -Response: - -```json -{ - "pane_id": "%4", - "pane_index": "1", - "pane_width": "39", - "pane_height": "24", - "pane_current_command": "zsh", - "pane_current_path": "/home/user/myproject", - "pane_pid": "3732", - "pane_title": "", - "pane_active": "0", - "window_id": "@0", - "session_id": "$0", - "is_caller": null -} -``` - -```{fastmcp-tool-input} window_tools.split_window -``` - ---- - -```{fastmcp-tool} window_tools.rename_window -``` - -**Use when** a window name no longer reflects its purpose. - -**Side effects:** Renames the window. - -**Example:** - -```json -{ - "tool": "rename_window", - "arguments": { - "session_name": "dev", - "new_name": "build" - } -} -``` - -Response: - -```json -{ - "window_id": "@0", - "window_name": "build", - "window_index": "1", - "session_id": "$0", - "session_name": "dev", - "pane_count": 2, - "window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]", - "window_active": "1", - "window_width": "80", - "window_height": "24" -} -``` - -```{fastmcp-tool-input} window_tools.rename_window -``` - ---- - -```{fastmcp-tool} window_tools.select_layout -``` - -**Use when** you want to rearrange panes — `even-horizontal`, -`even-vertical`, `main-horizontal`, `main-vertical`, or `tiled`. - -**Side effects:** Rearranges all panes in the window. - -**Example:** - -```json -{ - "tool": "select_layout", - "arguments": { - "session_name": "dev", - "layout": "even-vertical" - } -} -``` - -Response: - -```json -{ - "window_id": "@0", - "window_name": "editor", - "window_index": "1", - "session_id": "$0", - "session_name": "dev", - "pane_count": 2, - "window_layout": "even-vertical,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", - "window_active": "1", - "window_width": "80", - "window_height": "24" -} -``` - -```{fastmcp-tool-input} window_tools.select_layout -``` - ---- - -```{fastmcp-tool} window_tools.resize_window -``` - -**Use when** you need to adjust the window dimensions. - -**Side effects:** Changes window size. - -**Example:** - -```json -{ - "tool": "resize_window", - "arguments": { - "session_name": "dev", - "width": 120, - "height": 40 - } -} -``` - -Response: - -```json -{ - "window_id": "@0", - "window_name": "editor", - "window_index": "1", - "session_id": "$0", - "session_name": "dev", - "pane_count": 2, - "window_layout": "baaa,120x40,0,0[120x20,0,0,0,120x19,0,21,1]", - "window_active": "1", - "window_width": "120", - "window_height": "40" -} -``` - -```{fastmcp-tool-input} window_tools.resize_window -``` - ---- - -```{fastmcp-tool} window_tools.move_window -``` - -**Use when** you need to reorder windows within a session or move a window -to a different session entirely. - -**Side effects:** Changes the window's index or parent session. - -**Example:** - -```json -{ - "tool": "move_window", - "arguments": { - "window_id": "@1", - "destination_index": "1" - } -} -``` - -Response: - -```json -{ - "window_id": "@1", - "window_name": "server", - "window_index": "1", - "session_id": "$0", - "session_name": "dev", - "pane_count": 1, - "window_layout": "b25f,80x24,0,0,2", - "window_active": "0", - "window_width": "80", - "window_height": "24" -} -``` - -```{fastmcp-tool-input} window_tools.move_window -``` - -## Destroy - -```{fastmcp-tool} window_tools.kill_window -``` - -**Use when** you're done with a window and all its panes. - -**Avoid when** you only want to remove one pane — use {tooliconl}`kill-pane`. - -**Side effects:** Destroys the window and all its panes. Not reversible. - -**Example:** - -```json -{ - "tool": "kill_window", - "arguments": { - "window_id": "@1" - } -} -``` - -Response (string): - -```text -Window killed: @1 -``` - -```{fastmcp-tool-input} window_tools.kill_window -``` From 84c2cfd11453b830509f0939b5b14d41c8ca110a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 8 Apr 2026 18:37:05 -0500 Subject: [PATCH 3/4] docs(tools): Organize tool pages by tmux hierarchy Move tool pages from flat docs/tools/.md to hierarchy-based subdirectories: server/, session/, window/, pane/. - server/ (8): list-sessions, get-server-info, create-session, kill-server, show-option, set-option, show-environment, set-environment - session/ (5): list-windows, create-window, rename-session, select-window, kill-session - window/ (7): list-panes, split-window, rename-window, select-layout, resize-window, move-window, kill-window - pane/ (18): all pane interaction tools Update toctree paths to match new directory structure. --- docs/redirects.txt | 38 +++++ docs/tools/index.md | 42 +---- docs/tools/{ => pane}/capture-pane.md | 0 docs/tools/{ => pane}/clear-pane.md | 0 docs/tools/{ => pane}/display-message.md | 0 docs/tools/{ => pane}/enter-copy-mode.md | 0 docs/tools/{ => pane}/exit-copy-mode.md | 0 docs/tools/{ => pane}/get-pane-info.md | 0 docs/tools/pane/index.md | 155 ++++++++++++++++++ docs/tools/{ => pane}/kill-pane.md | 0 docs/tools/{ => pane}/paste-text.md | 0 docs/tools/{ => pane}/pipe-pane.md | 0 docs/tools/{ => pane}/resize-pane.md | 0 docs/tools/{ => pane}/search-panes.md | 0 docs/tools/{ => pane}/select-pane.md | 0 docs/tools/{ => pane}/send-keys.md | 0 docs/tools/{ => pane}/set-pane-title.md | 0 docs/tools/{ => pane}/snapshot-pane.md | 0 docs/tools/{ => pane}/swap-pane.md | 0 .../{ => pane}/wait-for-content-change.md | 0 docs/tools/{ => pane}/wait-for-text.md | 0 docs/tools/{ => server}/create-session.md | 0 docs/tools/{ => server}/get-server-info.md | 0 docs/tools/server/index.md | 69 ++++++++ docs/tools/{ => server}/kill-server.md | 0 docs/tools/{ => server}/list-sessions.md | 0 docs/tools/{ => server}/set-environment.md | 0 docs/tools/{ => server}/set-option.md | 0 docs/tools/{ => server}/show-environment.md | 0 docs/tools/{ => server}/show-option.md | 0 docs/tools/{ => session}/create-window.md | 0 docs/tools/session/index.md | 48 ++++++ docs/tools/{ => session}/kill-session.md | 0 docs/tools/{ => session}/list-windows.md | 0 docs/tools/{ => session}/rename-session.md | 0 docs/tools/{ => session}/select-window.md | 0 docs/tools/window/index.md | 62 +++++++ docs/tools/{ => window}/kill-window.md | 0 docs/tools/{ => window}/list-panes.md | 0 docs/tools/{ => window}/move-window.md | 0 docs/tools/{ => window}/rename-window.md | 0 docs/tools/{ => window}/resize-window.md | 0 docs/tools/{ => window}/select-layout.md | 0 docs/tools/{ => window}/split-window.md | 0 44 files changed, 376 insertions(+), 38 deletions(-) rename docs/tools/{ => pane}/capture-pane.md (100%) rename docs/tools/{ => pane}/clear-pane.md (100%) rename docs/tools/{ => pane}/display-message.md (100%) rename docs/tools/{ => pane}/enter-copy-mode.md (100%) rename docs/tools/{ => pane}/exit-copy-mode.md (100%) rename docs/tools/{ => pane}/get-pane-info.md (100%) create mode 100644 docs/tools/pane/index.md rename docs/tools/{ => pane}/kill-pane.md (100%) rename docs/tools/{ => pane}/paste-text.md (100%) rename docs/tools/{ => pane}/pipe-pane.md (100%) rename docs/tools/{ => pane}/resize-pane.md (100%) rename docs/tools/{ => pane}/search-panes.md (100%) rename docs/tools/{ => pane}/select-pane.md (100%) rename docs/tools/{ => pane}/send-keys.md (100%) rename docs/tools/{ => pane}/set-pane-title.md (100%) rename docs/tools/{ => pane}/snapshot-pane.md (100%) rename docs/tools/{ => pane}/swap-pane.md (100%) rename docs/tools/{ => pane}/wait-for-content-change.md (100%) rename docs/tools/{ => pane}/wait-for-text.md (100%) rename docs/tools/{ => server}/create-session.md (100%) rename docs/tools/{ => server}/get-server-info.md (100%) create mode 100644 docs/tools/server/index.md rename docs/tools/{ => server}/kill-server.md (100%) rename docs/tools/{ => server}/list-sessions.md (100%) rename docs/tools/{ => server}/set-environment.md (100%) rename docs/tools/{ => server}/set-option.md (100%) rename docs/tools/{ => server}/show-environment.md (100%) rename docs/tools/{ => server}/show-option.md (100%) rename docs/tools/{ => session}/create-window.md (100%) create mode 100644 docs/tools/session/index.md rename docs/tools/{ => session}/kill-session.md (100%) rename docs/tools/{ => session}/list-windows.md (100%) rename docs/tools/{ => session}/rename-session.md (100%) rename docs/tools/{ => session}/select-window.md (100%) create mode 100644 docs/tools/window/index.md rename docs/tools/{ => window}/kill-window.md (100%) rename docs/tools/{ => window}/list-panes.md (100%) rename docs/tools/{ => window}/move-window.md (100%) rename docs/tools/{ => window}/rename-window.md (100%) rename docs/tools/{ => window}/resize-window.md (100%) rename docs/tools/{ => window}/select-layout.md (100%) rename docs/tools/{ => window}/split-window.md (100%) diff --git a/docs/redirects.txt b/docs/redirects.txt index 4ac1aee..a5a3d7d 100644 --- a/docs/redirects.txt +++ b/docs/redirects.txt @@ -16,3 +16,41 @@ "tools/windows" "tools/index" "tools/panes" "tools/index" "tools/options" "tools/index" +"tools/capture-pane" "tools/pane/capture-pane" +"tools/clear-pane" "tools/pane/clear-pane" +"tools/create-session" "tools/server/create-session" +"tools/create-window" "tools/session/create-window" +"tools/display-message" "tools/pane/display-message" +"tools/enter-copy-mode" "tools/pane/enter-copy-mode" +"tools/exit-copy-mode" "tools/pane/exit-copy-mode" +"tools/get-pane-info" "tools/pane/get-pane-info" +"tools/get-server-info" "tools/server/get-server-info" +"tools/kill-pane" "tools/pane/kill-pane" +"tools/kill-server" "tools/server/kill-server" +"tools/kill-session" "tools/session/kill-session" +"tools/kill-window" "tools/window/kill-window" +"tools/list-panes" "tools/window/list-panes" +"tools/list-sessions" "tools/server/list-sessions" +"tools/list-windows" "tools/session/list-windows" +"tools/move-window" "tools/window/move-window" +"tools/paste-text" "tools/pane/paste-text" +"tools/pipe-pane" "tools/pane/pipe-pane" +"tools/rename-session" "tools/session/rename-session" +"tools/rename-window" "tools/window/rename-window" +"tools/resize-pane" "tools/pane/resize-pane" +"tools/resize-window" "tools/window/resize-window" +"tools/search-panes" "tools/pane/search-panes" +"tools/select-layout" "tools/window/select-layout" +"tools/select-pane" "tools/pane/select-pane" +"tools/select-window" "tools/session/select-window" +"tools/send-keys" "tools/pane/send-keys" +"tools/set-environment" "tools/server/set-environment" +"tools/set-option" "tools/server/set-option" +"tools/set-pane-title" "tools/pane/set-pane-title" +"tools/show-environment" "tools/server/show-environment" +"tools/show-option" "tools/server/show-option" +"tools/snapshot-pane" "tools/pane/snapshot-pane" +"tools/split-window" "tools/window/split-window" +"tools/swap-pane" "tools/pane/swap-pane" +"tools/wait-for-content-change" "tools/pane/wait-for-content-change" +"tools/wait-for-text" "tools/pane/wait-for-text" diff --git a/docs/tools/index.md b/docs/tools/index.md index 8c3e1ec..c3c7b98 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -300,42 +300,8 @@ Kill the entire tmux server. ```{toctree} :hidden: -capture-pane -clear-pane -create-session -create-window -display-message -enter-copy-mode -exit-copy-mode -get-pane-info -get-server-info -kill-pane -kill-server -kill-session -kill-window -list-panes -list-sessions -list-windows -move-window -paste-text -pipe-pane -rename-session -rename-window -resize-pane -resize-window -search-panes -select-layout -select-pane -select-window -send-keys -set-environment -set-option -set-pane-title -show-environment -show-option -snapshot-pane -split-window -swap-pane -wait-for-content-change -wait-for-text +server/index +session/index +window/index +pane/index ``` diff --git a/docs/tools/capture-pane.md b/docs/tools/pane/capture-pane.md similarity index 100% rename from docs/tools/capture-pane.md rename to docs/tools/pane/capture-pane.md diff --git a/docs/tools/clear-pane.md b/docs/tools/pane/clear-pane.md similarity index 100% rename from docs/tools/clear-pane.md rename to docs/tools/pane/clear-pane.md diff --git a/docs/tools/display-message.md b/docs/tools/pane/display-message.md similarity index 100% rename from docs/tools/display-message.md rename to docs/tools/pane/display-message.md diff --git a/docs/tools/enter-copy-mode.md b/docs/tools/pane/enter-copy-mode.md similarity index 100% rename from docs/tools/enter-copy-mode.md rename to docs/tools/pane/enter-copy-mode.md diff --git a/docs/tools/exit-copy-mode.md b/docs/tools/pane/exit-copy-mode.md similarity index 100% rename from docs/tools/exit-copy-mode.md rename to docs/tools/pane/exit-copy-mode.md diff --git a/docs/tools/get-pane-info.md b/docs/tools/pane/get-pane-info.md similarity index 100% rename from docs/tools/get-pane-info.md rename to docs/tools/pane/get-pane-info.md diff --git a/docs/tools/pane/index.md b/docs/tools/pane/index.md new file mode 100644 index 0000000..dab3682 --- /dev/null +++ b/docs/tools/pane/index.md @@ -0,0 +1,155 @@ +# Pane + +Tools for pane-level operations: reading content, sending input, navigation, scrollback, and lifecycle. + +## Inspect + +::::{grid} 1 2 3 3 +:gutter: 2 2 3 3 + +:::{grid-item-card} capture_pane +:link: capture-pane +:link-type: ref +Read visible content of a pane. +::: + +:::{grid-item-card} get_pane_info +:link: get-pane-info +:link-type: ref +Get detailed pane metadata. +::: + +:::{grid-item-card} search_panes +:link: search-panes +:link-type: ref +Search text across panes. +::: + +:::{grid-item-card} wait_for_text +:link: wait-for-text +:link-type: ref +Wait for text to appear in a pane. +::: + +:::{grid-item-card} snapshot_pane +:link: snapshot-pane +:link-type: ref +Rich capture: content + cursor + mode + scroll. +::: + +:::{grid-item-card} wait_for_content_change +:link: wait-for-content-change +:link-type: ref +Wait for any screen change. +::: + +:::{grid-item-card} display_message +:link: display-message +:link-type: ref +Query arbitrary tmux format strings. +::: + +:::: + +## Act + +::::{grid} 1 2 3 3 +:gutter: 2 2 3 3 + +:::{grid-item-card} send_keys +:link: send-keys +:link-type: ref +Send commands or keystrokes to a pane. +::: + +:::{grid-item-card} set_pane_title +:link: set-pane-title +:link-type: ref +Set pane title. +::: + +:::{grid-item-card} clear_pane +:link: clear-pane +:link-type: ref +Clear pane content. +::: + +:::{grid-item-card} resize_pane +:link: resize-pane +:link-type: ref +Adjust pane dimensions. +::: + +:::{grid-item-card} select_pane +:link: select-pane +:link-type: ref +Focus a pane by ID or direction. +::: + +:::{grid-item-card} swap_pane +:link: swap-pane +:link-type: ref +Exchange positions of two panes. +::: + +:::{grid-item-card} pipe_pane +:link: pipe-pane +:link-type: ref +Stream pane output to a file. +::: + +:::{grid-item-card} enter_copy_mode +:link: enter-copy-mode +:link-type: ref +Enter copy mode for scrollback. +::: + +:::{grid-item-card} exit_copy_mode +:link: exit-copy-mode +:link-type: ref +Exit copy mode. +::: + +:::{grid-item-card} paste_text +:link: paste-text +:link-type: ref +Paste multi-line text via tmux buffer. +::: + +:::: + +## Destroy + +::::{grid} 1 2 3 3 +:gutter: 2 2 3 3 + +:::{grid-item-card} kill_pane +:link: kill-pane +:link-type: ref +Destroy a pane. +::: + +:::: + +```{toctree} +:hidden: + +capture-pane +get-pane-info +search-panes +wait-for-text +snapshot-pane +wait-for-content-change +display-message +send-keys +set-pane-title +clear-pane +resize-pane +select-pane +swap-pane +pipe-pane +enter-copy-mode +exit-copy-mode +paste-text +kill-pane +``` diff --git a/docs/tools/kill-pane.md b/docs/tools/pane/kill-pane.md similarity index 100% rename from docs/tools/kill-pane.md rename to docs/tools/pane/kill-pane.md diff --git a/docs/tools/paste-text.md b/docs/tools/pane/paste-text.md similarity index 100% rename from docs/tools/paste-text.md rename to docs/tools/pane/paste-text.md diff --git a/docs/tools/pipe-pane.md b/docs/tools/pane/pipe-pane.md similarity index 100% rename from docs/tools/pipe-pane.md rename to docs/tools/pane/pipe-pane.md diff --git a/docs/tools/resize-pane.md b/docs/tools/pane/resize-pane.md similarity index 100% rename from docs/tools/resize-pane.md rename to docs/tools/pane/resize-pane.md diff --git a/docs/tools/search-panes.md b/docs/tools/pane/search-panes.md similarity index 100% rename from docs/tools/search-panes.md rename to docs/tools/pane/search-panes.md diff --git a/docs/tools/select-pane.md b/docs/tools/pane/select-pane.md similarity index 100% rename from docs/tools/select-pane.md rename to docs/tools/pane/select-pane.md diff --git a/docs/tools/send-keys.md b/docs/tools/pane/send-keys.md similarity index 100% rename from docs/tools/send-keys.md rename to docs/tools/pane/send-keys.md diff --git a/docs/tools/set-pane-title.md b/docs/tools/pane/set-pane-title.md similarity index 100% rename from docs/tools/set-pane-title.md rename to docs/tools/pane/set-pane-title.md diff --git a/docs/tools/snapshot-pane.md b/docs/tools/pane/snapshot-pane.md similarity index 100% rename from docs/tools/snapshot-pane.md rename to docs/tools/pane/snapshot-pane.md diff --git a/docs/tools/swap-pane.md b/docs/tools/pane/swap-pane.md similarity index 100% rename from docs/tools/swap-pane.md rename to docs/tools/pane/swap-pane.md diff --git a/docs/tools/wait-for-content-change.md b/docs/tools/pane/wait-for-content-change.md similarity index 100% rename from docs/tools/wait-for-content-change.md rename to docs/tools/pane/wait-for-content-change.md diff --git a/docs/tools/wait-for-text.md b/docs/tools/pane/wait-for-text.md similarity index 100% rename from docs/tools/wait-for-text.md rename to docs/tools/pane/wait-for-text.md diff --git a/docs/tools/create-session.md b/docs/tools/server/create-session.md similarity index 100% rename from docs/tools/create-session.md rename to docs/tools/server/create-session.md diff --git a/docs/tools/get-server-info.md b/docs/tools/server/get-server-info.md similarity index 100% rename from docs/tools/get-server-info.md rename to docs/tools/server/get-server-info.md diff --git a/docs/tools/server/index.md b/docs/tools/server/index.md new file mode 100644 index 0000000..f14d294 --- /dev/null +++ b/docs/tools/server/index.md @@ -0,0 +1,69 @@ +# Server + +Tools for server-level operations: session discovery, server info, configuration, and lifecycle. + +::::{grid} 1 2 3 3 +:gutter: 2 2 3 3 + +:::{grid-item-card} list_sessions +:link: list-sessions +:link-type: ref +List all active sessions. +::: + +:::{grid-item-card} get_server_info +:link: get-server-info +:link-type: ref +Get tmux server info. +::: + +:::{grid-item-card} create_session +:link: create-session +:link-type: ref +Start a new tmux session. +::: + +:::{grid-item-card} kill_server +:link: kill-server +:link-type: ref +Kill the entire tmux server. +::: + +:::{grid-item-card} show_option +:link: show-option +:link-type: ref +Query a tmux option value. +::: + +:::{grid-item-card} set_option +:link: set-option +:link-type: ref +Set a tmux option. +::: + +:::{grid-item-card} show_environment +:link: show-environment +:link-type: ref +Show tmux environment variables. +::: + +:::{grid-item-card} set_environment +:link: set-environment +:link-type: ref +Set a tmux environment variable. +::: + +:::: + +```{toctree} +:hidden: + +list-sessions +get-server-info +create-session +kill-server +show-option +set-option +show-environment +set-environment +``` diff --git a/docs/tools/kill-server.md b/docs/tools/server/kill-server.md similarity index 100% rename from docs/tools/kill-server.md rename to docs/tools/server/kill-server.md diff --git a/docs/tools/list-sessions.md b/docs/tools/server/list-sessions.md similarity index 100% rename from docs/tools/list-sessions.md rename to docs/tools/server/list-sessions.md diff --git a/docs/tools/set-environment.md b/docs/tools/server/set-environment.md similarity index 100% rename from docs/tools/set-environment.md rename to docs/tools/server/set-environment.md diff --git a/docs/tools/set-option.md b/docs/tools/server/set-option.md similarity index 100% rename from docs/tools/set-option.md rename to docs/tools/server/set-option.md diff --git a/docs/tools/show-environment.md b/docs/tools/server/show-environment.md similarity index 100% rename from docs/tools/show-environment.md rename to docs/tools/server/show-environment.md diff --git a/docs/tools/show-option.md b/docs/tools/server/show-option.md similarity index 100% rename from docs/tools/show-option.md rename to docs/tools/server/show-option.md diff --git a/docs/tools/create-window.md b/docs/tools/session/create-window.md similarity index 100% rename from docs/tools/create-window.md rename to docs/tools/session/create-window.md diff --git a/docs/tools/session/index.md b/docs/tools/session/index.md new file mode 100644 index 0000000..17053b3 --- /dev/null +++ b/docs/tools/session/index.md @@ -0,0 +1,48 @@ +# Session + +Tools for session-level operations: window management, session renaming, and lifecycle. + +::::{grid} 1 2 3 3 +:gutter: 2 2 3 3 + +:::{grid-item-card} list_windows +:link: list-windows +:link-type: ref +List windows in a session. +::: + +:::{grid-item-card} create_window +:link: create-window +:link-type: ref +Add a window to a session. +::: + +:::{grid-item-card} rename_session +:link: rename-session +:link-type: ref +Rename a session. +::: + +:::{grid-item-card} select_window +:link: select-window +:link-type: ref +Focus a window by ID, index, or direction. +::: + +:::{grid-item-card} kill_session +:link: kill-session +:link-type: ref +Destroy a session and all its windows. +::: + +:::: + +```{toctree} +:hidden: + +list-windows +create-window +rename-session +select-window +kill-session +``` diff --git a/docs/tools/kill-session.md b/docs/tools/session/kill-session.md similarity index 100% rename from docs/tools/kill-session.md rename to docs/tools/session/kill-session.md diff --git a/docs/tools/list-windows.md b/docs/tools/session/list-windows.md similarity index 100% rename from docs/tools/list-windows.md rename to docs/tools/session/list-windows.md diff --git a/docs/tools/rename-session.md b/docs/tools/session/rename-session.md similarity index 100% rename from docs/tools/rename-session.md rename to docs/tools/session/rename-session.md diff --git a/docs/tools/select-window.md b/docs/tools/session/select-window.md similarity index 100% rename from docs/tools/select-window.md rename to docs/tools/session/select-window.md diff --git a/docs/tools/window/index.md b/docs/tools/window/index.md new file mode 100644 index 0000000..bcf091b --- /dev/null +++ b/docs/tools/window/index.md @@ -0,0 +1,62 @@ +# Window + +Tools for window-level operations: pane management, layout, resizing, and lifecycle. + +::::{grid} 1 2 3 3 +:gutter: 2 2 3 3 + +:::{grid-item-card} list_panes +:link: list-panes +:link-type: ref +List panes in a window. +::: + +:::{grid-item-card} split_window +:link: split-window +:link-type: ref +Split a window into panes. +::: + +:::{grid-item-card} rename_window +:link: rename-window +:link-type: ref +Rename a window. +::: + +:::{grid-item-card} select_layout +:link: select-layout +:link-type: ref +Set window layout. +::: + +:::{grid-item-card} resize_window +:link: resize-window +:link-type: ref +Adjust window dimensions. +::: + +:::{grid-item-card} move_window +:link: move-window +:link-type: ref +Move window to another index or session. +::: + +:::{grid-item-card} kill_window +:link: kill-window +:link-type: ref +Destroy a window and all its panes. +::: + +:::: + +```{toctree} +:hidden: + +list-panes +split-window +rename-window +select-layout +resize-window +move-window +kill-window +``` diff --git a/docs/tools/kill-window.md b/docs/tools/window/kill-window.md similarity index 100% rename from docs/tools/kill-window.md rename to docs/tools/window/kill-window.md diff --git a/docs/tools/list-panes.md b/docs/tools/window/list-panes.md similarity index 100% rename from docs/tools/list-panes.md rename to docs/tools/window/list-panes.md diff --git a/docs/tools/move-window.md b/docs/tools/window/move-window.md similarity index 100% rename from docs/tools/move-window.md rename to docs/tools/window/move-window.md diff --git a/docs/tools/rename-window.md b/docs/tools/window/rename-window.md similarity index 100% rename from docs/tools/rename-window.md rename to docs/tools/window/rename-window.md diff --git a/docs/tools/resize-window.md b/docs/tools/window/resize-window.md similarity index 100% rename from docs/tools/resize-window.md rename to docs/tools/window/resize-window.md diff --git a/docs/tools/select-layout.md b/docs/tools/window/select-layout.md similarity index 100% rename from docs/tools/select-layout.md rename to docs/tools/window/select-layout.md diff --git a/docs/tools/split-window.md b/docs/tools/window/split-window.md similarity index 100% rename from docs/tools/split-window.md rename to docs/tools/window/split-window.md From c729553bae4c5ea7d2bbacf23bee65ae6b9ff42b Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Wed, 8 Apr 2026 18:37:33 -0500 Subject: [PATCH 4/4] docs(style): Strip badges from sidebar via SphinxContentsFilter Monkeypatch SphinxContentsFilter to raise SkipNode for all badge node types (_safety_badge_node, _resource_badge_node, _model_badge_node). This is the same pattern Sphinx uses for visit_image in titles. Badges stay inside nodes.title for proper heading rendering, but are stripped during toctree text extraction so the sidebar shows clean tool names without badge text or emoji leakage. Add CSS to size badges at 0.5em within h1/h2 headings for medium button appearance rather than full-scale heading text. --- docs/_ext/fastmcp_autodoc.py | 13 +++++++++++++ docs/_static/css/fastmcp_autodoc.css | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docs/_ext/fastmcp_autodoc.py b/docs/_ext/fastmcp_autodoc.py index c6fddcb..7bdbc69 100644 --- a/docs/_ext/fastmcp_autodoc.py +++ b/docs/_ext/fastmcp_autodoc.py @@ -1997,6 +1997,19 @@ def setup(app: Sphinx) -> ExtensionMetadata: # CSS app.add_css_file("css/fastmcp_autodoc.css") + # Strip badge nodes from toctree/sidebar title extraction. + # SphinxContentsFilter walks title nodes to build toctree text. + # Without this, badges inside titles propagate to the sidebar. + # This is the same pattern Sphinx uses for visit_image (SkipNode). + from sphinx.transforms import SphinxContentsFilter + + def _skip_badge(self: t.Any, node: nodes.Node) -> None: + raise nodes.SkipNode + + SphinxContentsFilter.visit__safety_badge_node = _skip_badge # type: ignore[attr-defined] + SphinxContentsFilter.visit__resource_badge_node = _skip_badge # type: ignore[attr-defined] + SphinxContentsFilter.visit__model_badge_node = _skip_badge # type: ignore[attr-defined] + return { "version": "0.1.0", "parallel_read_safe": True, diff --git a/docs/_static/css/fastmcp_autodoc.css b/docs/_static/css/fastmcp_autodoc.css index 14461e2..8edd903 100644 --- a/docs/_static/css/fastmcp_autodoc.css +++ b/docs/_static/css/fastmcp_autodoc.css @@ -18,6 +18,18 @@ span.sd-badge[aria-label*="model"] { font-family: var(--sd-fontfamily-monospace, monospace); } +/* Badge in section headings — medium size, not full h1 scale */ +h1 .sd-badge, +h2 .sd-badge { + font-size: 0.5em; + vertical-align: middle; +} + +/* Sidebar: strip background from inline code (tool/resource/model names) */ +.sidebar-tree code.literal { + background: none; +} + /* Field/param table polish — tighter padding */ .fastmcp-autodoc-table td, .fastmcp-autodoc-table th {