Skip to content

Commit 5d829dd

Browse files
committed
fix(engine): sanitize YAML date values in frontmatter to ISO strings
PyYAML safe_load converts date strings (e.g. '2026-03-06') into datetime.date objects, which are not JSON-serializable. This caused failures when frontmatter results were passed through the workflow engine's JSON serialization pipeline. - Add recursive _sanitize() in extract_markdown_frontmatter() - Convert datetime.date and datetime.datetime to ISO strings - Remove duplicate file_ext assignment in ReadFilesExecutor
1 parent 87842da commit 5d829dd

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

src/workflows_mcp/engine/executors_file.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,6 @@ async def _execute_single_file(
780780
file_path, mode
781781
)
782782
# Extract markdown intelligence for outline/summary mode
783-
file_ext = file_path.suffix.lower()
784783
if file_ext in (".md", ".markdown"):
785784
raw_content = file_path.read_text(encoding="utf-8", errors="replace")
786785
frontmatter = extract_markdown_frontmatter(raw_content)

src/workflows_mcp/engine/file_outline.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,28 @@ def extract_markdown_frontmatter(content: str) -> dict | None:
508508
return None
509509

510510
try:
511+
from datetime import date, datetime
512+
511513
import yaml as _yaml
512514

513515
result = _yaml.safe_load(yaml_text)
514-
return result if isinstance(result, dict) else None
516+
if not isinstance(result, dict):
517+
return None
518+
519+
# PyYAML safe_load converts date strings (e.g. "2026-03-06") to
520+
# datetime.date objects — sanitize to ISO strings for JSON safety.
521+
def _sanitize(obj: object) -> object:
522+
if isinstance(obj, datetime):
523+
return obj.isoformat()
524+
if isinstance(obj, date):
525+
return obj.isoformat()
526+
if isinstance(obj, dict):
527+
return {k: _sanitize(v) for k, v in obj.items()}
528+
if isinstance(obj, list):
529+
return [_sanitize(v) for v in obj]
530+
return obj
531+
532+
return _sanitize(result) # type: ignore[return-value]
515533
except Exception:
516534
return None
517535

tests/test_readfiles_sections.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,19 @@ def test_invalid_yaml(self) -> None:
327327
"""Invalid YAML returns None (no crash)."""
328328
assert extract_markdown_frontmatter("---\n[invalid: yaml:\n---\nContent\n") is None
329329

330+
def test_dates_converted_to_strings(self) -> None:
331+
"""YAML date values are converted to ISO strings for JSON safety."""
332+
import json
333+
334+
content = "---\ncreated: 2026-03-06\ntitle: Test\n---\n# Title\n"
335+
result = extract_markdown_frontmatter(content)
336+
337+
assert result is not None
338+
assert isinstance(result["created"], str)
339+
assert result["created"] == "2026-03-06"
340+
# Must be JSON-serializable
341+
json.dumps(result) # should not raise
342+
330343

331344
# ── extract_markdown_references ────────────────────────────────────────
332345

0 commit comments

Comments
 (0)