|
| 1 | +#!/bin/bash |
| 2 | +set -e |
| 3 | + |
| 4 | +python3 - << 'EOF' |
| 5 | +""" |
| 6 | +MCP TOOLBOX: TOOL PAGE LINTER |
| 7 | +============================= |
| 8 | +This script enforces a standardized structure for individual Tool pages |
| 9 | +and their parent directory wrappers. It ensures LLM agents can parse |
| 10 | +tool capabilities and parameter definitions reliably. |
| 11 | +
|
| 12 | +MAINTENANCE GUIDE: |
| 13 | +------------------ |
| 14 | +1. TO ADD A NEW HEADING: |
| 15 | + Add the exact heading text to the 'ALLOWED_ORDER' list in the desired |
| 16 | + sequence. |
| 17 | +
|
| 18 | +2. TO MAKE A HEADING MANDATORY/OPTIONAL: |
| 19 | + Add or remove the heading text in the 'REQUIRED' set. |
| 20 | +
|
| 21 | +3. TO UPDATE SHORTCODE LOGIC: |
| 22 | + If the shortcode name changes, update the 'SHORTCODE_PATTERN' variable. |
| 23 | +
|
| 24 | +4. SCOPE & BEHAVIOR: |
| 25 | + This script targets all .md files in docs/en/integrations/**/tools/. |
| 26 | + - For `_index.md` files: It only validates the frontmatter (requiring |
| 27 | + `title: "Tools"` and `weight: 2`) and ignores the body. |
| 28 | + - For regular tool files: It validates H1/H2 hierarchy, checks for |
| 29 | + required headings ("About", "Example"), and enforces that the |
| 30 | + `{{< compatible-sources >}}` shortcode is paired with the |
| 31 | + "## Compatible Sources" heading. |
| 32 | +""" |
| 33 | +
|
| 34 | +import os |
| 35 | +import re |
| 36 | +import sys |
| 37 | +from pathlib import Path |
| 38 | +
|
| 39 | +# --- CONFIGURATION --- |
| 40 | +ALLOWED_ORDER = [ |
| 41 | + "About", |
| 42 | + "Compatible Sources", |
| 43 | + "Requirements", |
| 44 | + "Parameters", |
| 45 | + "Example", |
| 46 | + "Output Format", |
| 47 | + "Reference", |
| 48 | + "Advanced Usage", |
| 49 | + "Troubleshooting", |
| 50 | + "Additional Resources" |
| 51 | +] |
| 52 | +REQUIRED = {"About", "Example"} |
| 53 | +SHORTCODE_PATTERN = r"\{\{<\s*compatible-sources.*?>\}\}" |
| 54 | +# --------------------- |
| 55 | +
|
| 56 | +integration_dir = Path("./docs/en/integrations") |
| 57 | +if not integration_dir.exists(): |
| 58 | + print("Info: Directory './docs/en/integrations' not found. Skipping linting.") |
| 59 | + sys.exit(0) |
| 60 | +
|
| 61 | +has_errors = False |
| 62 | +tools_pages_found = 0 |
| 63 | +
|
| 64 | +# Specifically target the tools directories |
| 65 | +for filepath in integration_dir.rglob("tools/*.md"): |
| 66 | + tools_pages_found += 1 |
| 67 | + with open(filepath, "r", encoding="utf-8") as f: |
| 68 | + content = f.read() |
| 69 | +
|
| 70 | + # Separate YAML frontmatter from the markdown body |
| 71 | + match = re.match(r'^\s*---\s*\n(.*?)\n---\s*(.*)', content, re.DOTALL) |
| 72 | + if match: |
| 73 | + frontmatter = match.group(1) |
| 74 | + body = match.group(2) |
| 75 | + else: |
| 76 | + print(f"[{filepath}] Error: Missing or invalid YAML frontmatter.") |
| 77 | + has_errors = True |
| 78 | + continue |
| 79 | +
|
| 80 | + file_errors = False |
| 81 | +
|
| 82 | + # --- SPECIAL VALIDATION FOR tools/_index.md --- |
| 83 | + if filepath.name == "_index.md": |
| 84 | + title_match = re.search(r"^title:\s*[\"']?(.*?)[\"']?\s*$", frontmatter, re.MULTILINE) |
| 85 | + if not title_match or title_match.group(1).strip() != "Tools": |
| 86 | + print(f"[{filepath}] Error: tools/_index.md must have exactly title: \"Tools\"") |
| 87 | + file_errors = True |
| 88 | +
|
| 89 | + weight_match = re.search(r"^weight:\s*(\d+)\s*$", frontmatter, re.MULTILINE) |
| 90 | + if not weight_match or weight_match.group(1).strip() != "2": |
| 91 | + print(f"[{filepath}] Error: tools/_index.md must have exactly weight: 2") |
| 92 | + file_errors = True |
| 93 | +
|
| 94 | + if file_errors: |
| 95 | + has_errors = True |
| 96 | + continue # Skip the rest of the body linting for this structural file |
| 97 | +
|
| 98 | + # --- VALIDATION FOR REGULAR TOOL PAGES --- |
| 99 | + # If the file has no markdown content (metadata placeholder only), skip it entirely |
| 100 | + if not body.strip(): |
| 101 | + continue |
| 102 | +
|
| 103 | + # 1. Check Shortcode Placement |
| 104 | + sources_section_match = re.search(r"^##\s+Compatible Sources\s*(.*?)(?=^##\s|\Z)", body, re.MULTILINE | re.DOTALL) |
| 105 | + if sources_section_match: |
| 106 | + if not re.search(SHORTCODE_PATTERN, sources_section_match.group(1)): |
| 107 | + print(f"[{filepath}] Error: The compatible-sources shortcode must be placed under '## Compatible Sources'.") |
| 108 | + file_errors = True |
| 109 | + elif re.search(SHORTCODE_PATTERN, body): |
| 110 | + print(f"[{filepath}] Error: Shortcode found, but '## Compatible Sources' heading is missing.") |
| 111 | + file_errors = True |
| 112 | +
|
| 113 | + # 2. Strip code blocks from body to avoid linting example markdown headings |
| 114 | + clean_body = re.sub(r"```.*?```", "", body, flags=re.DOTALL) |
| 115 | +
|
| 116 | + # 3. Check H1 Headings |
| 117 | + if re.search(r"^#\s+\w+", clean_body, re.MULTILINE): |
| 118 | + print(f"[{filepath}] Error: H1 headings (#) are forbidden in the body.") |
| 119 | + file_errors = True |
| 120 | +
|
| 121 | + # 4. Check H2 Headings |
| 122 | + h2s = re.findall(r"^##\s+(.*)", clean_body, re.MULTILINE) |
| 123 | + h2s = [h2.strip() for h2 in h2s] |
| 124 | +
|
| 125 | + # Missing Required |
| 126 | + if missing := (REQUIRED - set(h2s)): |
| 127 | + print(f"[{filepath}] Error: Missing required H2 headings: {missing}") |
| 128 | + file_errors = True |
| 129 | +
|
| 130 | + # Unauthorized Headings |
| 131 | + if unauthorized := (set(h2s) - set(ALLOWED_ORDER)): |
| 132 | + print(f"[{filepath}] Error: Unauthorized H2 headings found: {unauthorized}") |
| 133 | + file_errors = True |
| 134 | +
|
| 135 | + # Strict Ordering |
| 136 | + filtered_h2s = [h for h in h2s if h in ALLOWED_ORDER] |
| 137 | + expected_order = [h for h in ALLOWED_ORDER if h in h2s] |
| 138 | + if filtered_h2s != expected_order: |
| 139 | + print(f"[{filepath}] Error: Headings are out of order.") |
| 140 | + print(f" Expected: {expected_order}") |
| 141 | + print(f" Found: {filtered_h2s}") |
| 142 | + file_errors = True |
| 143 | +
|
| 144 | + if file_errors: |
| 145 | + has_errors = True |
| 146 | +
|
| 147 | +if tools_pages_found == 0: |
| 148 | + print("Info: No tool directories found. Passing gracefully.") |
| 149 | + sys.exit(0) |
| 150 | +elif has_errors: |
| 151 | + print("Linting failed for Tool pages. Please fix the structure errors above.") |
| 152 | + sys.exit(1) |
| 153 | +else: |
| 154 | + print(f"Success: All {tools_pages_found} Tool page(s) passed structure validation.") |
| 155 | +EOF |
0 commit comments