From 124b10e2bb6c0a9ea0ce64a7e1442dc6a4ee8539 Mon Sep 17 00:00:00 2001 From: Sai RaM Date: Thu, 6 Aug 2026 12:34:18 +0530 Subject: [PATCH] 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 --- .../server/providers/skills/_common.py | 13 +++- tests/server/providers/test_skills_common.py | 66 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tests/server/providers/test_skills_common.py diff --git a/fastmcp_slim/fastmcp/server/providers/skills/_common.py b/fastmcp_slim/fastmcp/server/providers/skills/_common.py index 340d289e5..86aa7c29e 100644 --- a/fastmcp_slim/fastmcp/server/providers/skills/_common.py +++ b/fastmcp_slim/fastmcp/server/providers/skills/_common.py @@ -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: diff --git a/tests/server/providers/test_skills_common.py b/tests/server/providers/test_skills_common.py new file mode 100644 index 000000000..38ba83f64 --- /dev/null +++ b/tests/server/providers/test_skills_common.py @@ -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 == {} \ No newline at end of file