@@ -376,6 +376,15 @@ def _check_stdio(where: str, entry: dict, root: Path, report: Report) -> None:
376376 where ,
377377 f"command { command !r} escapes the plugin root (§4.1)." ,
378378 )
379+ elif "/" in command :
380+ # §7.2.1: command is either a bare executable name (resolved by platform
381+ # search) or a plugin-relative path beginning with ./ . An absolute path
382+ # or a ../ traversal is neither and must not pass as PATH-resolved.
383+ report .fail (
384+ where ,
385+ f"command { command !r} must be a bare executable name or a plugin-relative "
386+ f"path beginning with './' (§7.2.1)." ,
387+ )
379388
380389 args = entry .get ("args" )
381390 if args is not None and (
@@ -581,11 +590,18 @@ def check_skill_md(where: str, skill_dir: Path, path: Path, report: Report) -> N
581590 report .fail (where , "unterminated YAML frontmatter" )
582591 return
583592
593+ # Extract `key: value` pairs. A value that is empty or comment-only
594+ # (`description: # omitted`) is not a value; treat it as absent so the
595+ # required-field checks fire instead of silently accepting a comment.
584596 fields : dict [str , str ] = {}
585597 for line in lines [1 :end ]:
586598 match = re .match (r"^([A-Za-z_-]+):\s*(.*)$" , line )
587599 if match :
588- fields [match .group (1 )] = match .group (2 ).strip ().strip ('"' ).strip ("'" )
600+ raw = match .group (2 ).strip ()
601+ # a value that is empty or comment-only (`description: # omitted`)
602+ # is not a value; a trailing ` # comment` is stripped too
603+ raw = "" if raw .startswith ("#" ) else re .sub (r"\s+#.*$" , "" , raw ).strip ()
604+ fields [match .group (1 )] = raw .strip ('"' ).strip ("'" )
589605
590606 name = fields .get ("name" , "" )
591607 if not name :
@@ -605,8 +621,16 @@ def check_skill_md(where: str, skill_dir: Path, path: Path, report: Report) -> N
605621 f"frontmatter name { name !r} does not match its directory "
606622 f"{ skill_dir .name !r} ; discovery is by directory (§7.1)." ,
607623 )
608- if not fields .get ("description" ):
624+ description = fields .get ("description" , "" )
625+ if not description :
609626 report .fail (where , "frontmatter is missing `description`" )
627+ elif len (description ) > 1024 :
628+ # Agent Skills limits `description` to 1,024 characters; a client may
629+ # reject a skill that the validator accepted.
630+ report .fail (
631+ where ,
632+ f"frontmatter description is { len (description )} characters, must be at most 1024" ,
633+ )
610634
611635
612636# --- client extension directories (§8.2) ----------------------------------
@@ -759,12 +783,6 @@ def validate(root: Path) -> Report:
759783 report .note (f"{ count } skill(s) discovered under skills/" )
760784 check_client_extensions (root , report )
761785 check_parity (root , plugin , report )
762- if (root / ".mcp.json" ).is_file () and not (root / "mcp.json" ).is_file ():
763- report .note (
764- "`.mcp.json` is present but `mcp.json` is not; Agent Plugins reads only "
765- "`mcp.json` at the plugin root, so these MCP servers are invisible to a "
766- "conformant client (§6.1, §7.2.1)."
767- )
768786 return report
769787
770788
0 commit comments