|
| 1 | +"""Export the TestGen MCP server as a Markdown reference page. |
| 2 | +
|
| 3 | +Usage: |
| 4 | + python deploy/build_mcp_docs.py [--output PATH] |
| 5 | +
|
| 6 | +Introspects the FastMCP instance built by ``build_mcp_server()`` and emits |
| 7 | +a single Markdown page listing prompts, tools, and resources. Tools are |
| 8 | +grouped by the ``_DOC_GROUP`` constant defined on each tool module — when |
| 9 | +adding a new tool module, declare ``_DOC_GROUP = "..."`` so the new tools |
| 10 | +land under the right heading automatically. |
| 11 | +""" |
| 12 | + |
| 13 | +import argparse |
| 14 | +import re |
| 15 | +import sys |
| 16 | +import textwrap |
| 17 | +from pathlib import Path |
| 18 | +from typing import Any |
| 19 | + |
| 20 | +from testgen.mcp.server import build_mcp_server |
| 21 | +from testgen.mcp.tools.common import DocGroup |
| 22 | + |
| 23 | +_DEFAULT_OUTPUT = Path("docs/mcp/supported-tools.md") |
| 24 | +_ARGS_HEADER_RE = re.compile(r"^\s*Args:\s*$", re.MULTILINE) |
| 25 | + |
| 26 | +# Order in which tool groups appear on the page. Each entry is a ``DocGroup`` |
| 27 | +# member; tools whose module declares a ``_DOC_GROUP`` not in this list are |
| 28 | +# appended after these in the order they are first seen. |
| 29 | +_GROUP_ORDER: list[DocGroup] = [ |
| 30 | + DocGroup.DISCOVER, |
| 31 | + DocGroup.INVESTIGATE, |
| 32 | + DocGroup.BROWSE_PROFILING, |
| 33 | + DocGroup.TRIGGER, |
| 34 | +] |
| 35 | +_FALLBACK_GROUP = "Other tools" |
| 36 | + |
| 37 | + |
| 38 | +def _short_description(docstring: str) -> str: |
| 39 | + """Return the first prose paragraph of a docstring, stripped of Args/Returns sections.""" |
| 40 | + if not docstring: |
| 41 | + return "" |
| 42 | + text = textwrap.dedent(docstring).strip() |
| 43 | + match = _ARGS_HEADER_RE.search(text) |
| 44 | + if match: |
| 45 | + text = text[: match.start()].rstrip() |
| 46 | + first_paragraph = text.split("\n\n", 1)[0] |
| 47 | + return " ".join(line.strip() for line in first_paragraph.splitlines()) |
| 48 | + |
| 49 | + |
| 50 | +def _entry_name(item: Any) -> str: |
| 51 | + """Display name for a tool, resource, or prompt.""" |
| 52 | + return str(getattr(item, "uri", None) or item.name) |
| 53 | + |
| 54 | + |
| 55 | +def _render_entry(item: Any) -> str: |
| 56 | + description = _short_description(item.description or "") |
| 57 | + return f"- **`{_entry_name(item)}`** — {description}" |
| 58 | + |
| 59 | + |
| 60 | +def _group_for_tool(tool: Any) -> str: |
| 61 | + """Resolve a tool's display group via its module's ``_DOC_GROUP`` constant.""" |
| 62 | + module = sys.modules.get(tool.fn.__module__) |
| 63 | + group = getattr(module, "_DOC_GROUP", None) |
| 64 | + return str(group) if group is not None else _FALLBACK_GROUP |
| 65 | + |
| 66 | + |
| 67 | +def _group_tools(tools: list[Any]) -> list[tuple[str, list[Any]]]: |
| 68 | + """Bucket tools by their module's ``_DOC_GROUP``, ordered by ``_GROUP_ORDER``.""" |
| 69 | + buckets: dict[str, list[Any]] = {} |
| 70 | + for tool in tools: |
| 71 | + buckets.setdefault(_group_for_tool(tool), []).append(tool) |
| 72 | + |
| 73 | + ordered: list[tuple[str, list[Any]]] = [] |
| 74 | + for group in _GROUP_ORDER: |
| 75 | + title = str(group) |
| 76 | + if title in buckets: |
| 77 | + ordered.append((title, sorted(buckets.pop(title), key=lambda t: t.name))) |
| 78 | + for title, bucket in buckets.items(): |
| 79 | + ordered.append((title, sorted(bucket, key=lambda t: t.name))) |
| 80 | + return ordered |
| 81 | + |
| 82 | + |
| 83 | +def _build_markdown(mcp: Any) -> str: |
| 84 | + tools = mcp._tool_manager.list_tools() |
| 85 | + resources = sorted(mcp._resource_manager.list_resources(), key=lambda r: str(r.uri)) |
| 86 | + prompts = sorted(mcp._prompt_manager.list_prompts(), key=lambda p: p.name) |
| 87 | + grouped_tools = _group_tools(list(tools)) |
| 88 | + |
| 89 | + parts: list[str] = [ |
| 90 | + "# Supported Tools", |
| 91 | + "", |
| 92 | + "The TestGen MCP server exposes the prompts, tools, and resources listed below.", |
| 93 | + "", |
| 94 | + "For setup instructions, see [Set up the MCP Server](setup.md).", |
| 95 | + "For example questions to ask an assistant, see [MCP Server](index.md#what-you-can-ask).", |
| 96 | + "", |
| 97 | + "## Prompts", |
| 98 | + "", |
| 99 | + ( |
| 100 | + "Prompts are pre-built workflows you can invoke directly through your AI client — typically " |
| 101 | + "as a slash command (for example, `/testgen:table_health` in Claude Code) or " |
| 102 | + "from a quick-action menu. They orchestrate several tool calls behind the scenes for common " |
| 103 | + "investigations. Exact UX varies by client." |
| 104 | + ), |
| 105 | + "", |
| 106 | + ] |
| 107 | + parts.extend(_render_entry(prompt) for prompt in prompts) |
| 108 | + parts.append("") |
| 109 | + |
| 110 | + parts.extend(["## Tools", "", "Tools are operations the assistant calls during a conversation, picked based on what you ask.", ""]) |
| 111 | + for heading, bucket in grouped_tools: |
| 112 | + parts.append(f"### {heading}") |
| 113 | + parts.append("") |
| 114 | + parts.extend(_render_entry(tool) for tool in bucket) |
| 115 | + parts.append("") |
| 116 | + |
| 117 | + parts.extend( |
| 118 | + [ |
| 119 | + "## Resources", |
| 120 | + "", |
| 121 | + "Resources are static reference documents that AI clients can fetch by URI.", |
| 122 | + "", |
| 123 | + ] |
| 124 | + ) |
| 125 | + parts.extend(_render_entry(resource) for resource in resources) |
| 126 | + |
| 127 | + return "\n".join(parts).rstrip() + "\n" |
| 128 | + |
| 129 | + |
| 130 | +def main() -> None: |
| 131 | + parser = argparse.ArgumentParser(description="Export the TestGen MCP server as a Markdown reference.") |
| 132 | + parser.add_argument( |
| 133 | + "--output", |
| 134 | + type=Path, |
| 135 | + default=_DEFAULT_OUTPUT, |
| 136 | + help=f"Output Markdown file path (default: {_DEFAULT_OUTPUT}, relative to cwd)", |
| 137 | + ) |
| 138 | + args = parser.parse_args() |
| 139 | + |
| 140 | + mcp = build_mcp_server(api_base_url="https://testgen.example.com") |
| 141 | + markdown = _build_markdown(mcp) |
| 142 | + |
| 143 | + output: Path = args.output |
| 144 | + output.parent.mkdir(parents=True, exist_ok=True) |
| 145 | + frontmatter = "---\nsearch:\n boost: 0.5\n---\n" |
| 146 | + output.write_text(frontmatter + markdown, encoding="utf-8") |
| 147 | + print(f"Exported MCP supported tools -> {output}") |
| 148 | + |
| 149 | + |
| 150 | +if __name__ == "__main__": |
| 151 | + main() |
0 commit comments