fix: use yaml.BaseLoader for skills frontmatter parsing

- Replaces simple line-by-line parser with yaml.BaseLoader
- BaseLoader supports block scalars (|, >) without implicit type coercion
- No yes->True, no 1.10->1.1, scalar text preserved as-is
- Falls back to original line parser for malformed YAML
- Adds tests for multiline, type preservation, fallback, and other keys

Fixes #4416
This commit is contained in:
Sai RaM 2026-08-06 12:34:18 +05:30
commit 124b10e2bb
2 changed files with 78 additions and 1 deletions

View file

@ -52,7 +52,18 @@ def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]:
frontmatter_text = content[3 : 3 + end_match.start()]
remaining = content[3 + end_match.end() :]
# Parse YAML (simple key: value parsing, no complex types)
# Try parsing with yaml.BaseLoader first.
# BaseLoader supports block scalars (|, >) without implicit type coercion
# (no yes->True, no 1.10->1.1), making it safe for untrusted frontmatter.
try:
import yaml
parsed = yaml.load(frontmatter_text, Loader=yaml.BaseLoader)
if isinstance(parsed, dict):
return parsed, remaining
except Exception:
pass
# Fall back to simple line-by-line parsing for malformed YAML
frontmatter: dict[str, Any] = {}
for line in frontmatter_text.strip().split("\n"):
if ":" in line:

View file

@ -0,0 +1,66 @@
"""Tests for skills frontmatter parsing."""
from __future__ import annotations
import pytest
from fastmcp.server.providers.skills._common import parse_frontmatter
class TestParseFrontmatter:
def test_single_line_description(self):
content = "---\ndescription: A simple description\n---\n# Body"
frontmatter, remaining = parse_frontmatter(content)
assert frontmatter["description"] == "A simple description"
assert "# Body" in remaining
def test_multiline_description_pipe(self):
content = "---\ndescription: |\n First line.\n Second line.\n---\n# Body"
frontmatter, remaining = parse_frontmatter(content)
assert "First line." in frontmatter["description"]
assert "Second line." in frontmatter["description"]
def test_multiline_description_folded(self):
content = "---\ndescription: >\n First line.\n Second line.\n---\n# Body"
frontmatter, remaining = parse_frontmatter(content)
assert frontmatter["description"] != ""
def test_no_type_coercion_yes(self):
content = "---\ndescription: yes\n---\n"
frontmatter, _ = parse_frontmatter(content)
assert frontmatter["description"] == "yes"
assert isinstance(frontmatter["description"], str)
def test_no_type_coercion_true(self):
content = "---\ndescription: true\n---\n"
frontmatter, _ = parse_frontmatter(content)
assert frontmatter["description"] == "true"
assert isinstance(frontmatter["description"], str)
def test_no_type_coercion_float(self):
content = "---\nversion: 1.10\n---\n"
frontmatter, _ = parse_frontmatter(content)
assert frontmatter["version"] == "1.10"
assert isinstance(frontmatter["version"], str)
def test_other_keys_preserved(self):
content = "---\nname: my-skill\nauthor: sai\n---\n"
frontmatter, _ = parse_frontmatter(content)
assert frontmatter["name"] == "my-skill"
assert frontmatter["author"] == "sai"
def test_malformed_yaml_falls_back(self):
content = "---\ndescription: value: with: colons\n---\n"
frontmatter, _ = parse_frontmatter(content)
assert "description" in frontmatter
def test_no_frontmatter_returns_empty(self):
content = "# Just markdown\nNo frontmatter here."
frontmatter, remaining = parse_frontmatter(content)
assert frontmatter == {}
assert remaining == content
def test_empty_frontmatter_returns_empty_dict(self):
content = "---\n---\n# Body"
frontmatter, remaining = parse_frontmatter(content)
assert frontmatter == {}