mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 05:24:18 +02:00
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:
parent
875e8e18bd
commit
124b10e2bb
2 changed files with 78 additions and 1 deletions
|
|
@ -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:
|
||||
|
|
|
|||
66
tests/server/providers/test_skills_common.py
Normal file
66
tests/server/providers/test_skills_common.py
Normal 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 == {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue