list_sections
List all section headers within a large documentation file to identify the correct header before retrieving a specific section.
Instructions
List all section headers in a doc file. Use before get_section() to find the right header.
Especially useful for large files like ta.md, strategy.md, collections.md, drawing.md, general.md which have 50-115 sections each.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Documentation file path (e.g., "reference/functions/ta.md") |
Implementation Reference
- src/pinescript_mcp/server.py:435-460 (handler)The `list_sections` tool handler: a FastMCP tool decorated with @mcp.tool that accepts a doc file path, validates it, reads the file content, extracts top-level (##) section headers, and returns them as a newline-separated string. Uses _timed_tool for metrics/logging.
@mcp.tool( tags={"reference", "discovery"}, annotations={"readOnlyHint": True, "idempotentHint": True, "openWorldHint": False} ) async def list_sections(path: str): """List all section headers in a doc file. Use before get_section() to find the right header. Especially useful for large files like ta.md, strategy.md, collections.md, drawing.md, general.md which have 50-115 sections each. Args: path: Documentation file path (e.g., "reference/functions/ta.md") Returns top-level section headers (## level) for navigation. Subsections (###) are omitted since get_section(include_children=True) returns them when reading. """ with _timed_tool("list_sections", path=path) as log: try: _validate_path(path) # check path is allowed content = _get_doc_content(path) headers = [line for line in content.splitlines() if line.startswith("#") and not line.startswith("###")] log["headers_found"] = len(headers) return "\n".join(headers) except ValueError as e: log["error"] = str(e) raise ToolError(str(e)) - src/pinescript_mcp/server.py:435-438 (registration)The `@mcp.tool` decorator that registers the function as an MCP tool, with tags={'reference', 'discovery'} and annotations for readOnly, idempotent, and openWorld hints.
@mcp.tool( tags={"reference", "discovery"}, annotations={"readOnlyHint": True, "idempotentHint": True, "openWorldHint": False} ) - src/pinescript_mcp/server.py:239-241 (helper)Helper `_get_doc_content()` that reads cached doc file content as a single string, used by list_sections to get the file content.
def _get_doc_content(rel_path: str) -> str: """Return doc file as a single string, cached after first read.""" return "\n".join(_get_doc_lines(rel_path)) - src/pinescript_mcp/server.py:359-384 (helper)Helper `_validate_path()` that validates the path is allowed and exists within the docs root, used by list_sections for path security.
def _validate_path(path: str) -> Path: """Validate and resolve a documentation path. Raises ValueError if invalid.""" # Normalize path clean_path = path.lstrip("/").lstrip("./") # Check for path traversal if ".." in clean_path: raise ValueError(f"Invalid path: {path}") # Check if in allowed directory allowed = any(clean_path.startswith(d) for d in ALLOWED_DIRS) if not allowed: raise ValueError(f"Path not in allowed directories: {path}") full_path = DOCS_ROOT / clean_path # Verify path is within docs root try: full_path.resolve().relative_to(DOCS_ROOT.resolve()) except ValueError: raise ValueError(f"Path escapes documentation root: {path}") if not full_path.exists(): raise ValueError(f"File not found: {path}") return full_path - src/pinescript_mcp/server.py:97-130 (helper)Helper `_timed_tool` context manager used for timing, logging, and Prometheus metrics of the list_sections tool call.
class _timed_tool: """Context manager for tool timing and logging. Usage: with _timed_tool("get_doc", path=path) as log: ... log["chars"] = len(content) # add extra fields """ def __init__(self, tool_name: str, **kwargs): self._tool_name = tool_name self._extra = kwargs self._data: dict = {} def __enter__(self): self._start = time.time() self._data = {} return self._data def __exit__(self, exc_type, exc_val, exc_tb): duration = time.time() - self._start transport = _current_transport.get() or _TRANSPORT tool_calls_total.labels(tool=self._tool_name, transport=transport, region=_FLY_REGION).inc() tool_duration_seconds.labels(tool=self._tool_name, transport=transport, region=_FLY_REGION).observe(duration) if exc_type is not None: tool_errors_total.labels(tool=self._tool_name, transport=transport, region=_FLY_REGION).inc() log_data = { "event": "tool_call", "tool": self._tool_name, **self._extra, **self._data, "duration_ms": int(duration * 1000), } _logger.info(json.dumps(log_data)) return False