Description
An MCP skills index advertises N skills. MCPSkillsSource.get_skills() can return fewer, and when it does there is nothing above DEBUG to say which ones went missing or why. A skill that failed to load is indistinguishable, from the caller's side, from a skill that was never published.
Every refusal path in _ArchiveEntryLoader logs at logger.debug:
- the index entry is missing
name, or missing url
- the archive resource returns no binary content, or empty content
- the archive exceeds
archive_max_file_count, archive_max_size_bytes or archive_max_uncompressed_size_bytes
- the media type is unsupported
- the archive contains no
SKILL.md
SKILL.md is not valid UTF-8
- the
SKILL.md frontmatter name does not match the advertised entry name
The asymmetry inside that same class is what makes this look unintended rather than deliberate: a duplicate skill name logs at logger.warning, and a failed archive read that is not an MCP -32002 logs at logger.warning before propagating. So the class already reaches for WARNING when it judges something notable — it just classifies "I refused to load a skill your index advertised" as routine.
To reproduce
- Publish a multi-file skill package to an MCP skills server so it is indexed with
type: "archive".
- Give its
SKILL.md frontmatter a name that differs from the index entry name — or let the package exceed archive_max_file_count (default 20).
- Call
MCPSkillsSource.get_skills() at the default log level.
The skill is absent from the returned list. Nothing is logged above DEBUG.
Expected behavior
Refusing these packages is correct — a package whose internal name disagrees with the advertised one is a genuine integrity concern, and the extraction caps are DoS guards worth keeping. The refusal is not the problem; the silence is. A caller cannot act on a skill that vanished without knowing it vanished.
Raising the "refused a package the index advertised" paths to WARNING would resolve it, with no behavioral change and no API change. A single summary line at the end of discovery — advertised count versus loaded count, naming the difference — would cover every path above in one place and would arguably be the better shape, since it also catches whatever refusal paths get added later.
The distinction worth preserving: a malformed index entry is the server's fault and may reasonably stay quiet, but an entry that was well-formed enough to attempt and then failed extraction is a package the operator meant to ship.
Impact
Discovered in production. A toolbox advertising 16 skills logged Successfully loaded 15 skills from MCP server and served 15; the sixteenth was silently absent for an unknown period. The agent behaved as though that capability did not exist, which surfaces as degraded answers rather than as an error, so nothing draws attention to it.
Three of the refusal paths are configurable and can be given headroom via the archive_max_* constructor arguments. The frontmatter/index name mismatch is not configurable, so for that path there is no setting an operator can use to avoid the silence.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.13.0
Python Version
No response
Additional Context
Additional context
The caps default to archive_max_file_count=20, archive_max_size_bytes=1 MB, archive_max_uncompressed_size_bytes=1 MB. A package sitting just under the file cap crosses it on an ordinary content edit, so this is reachable without anything unusual happening — the failure arrives on a routine change and is silent when it does.
This is a follow-up to #6088 (Support MCP skills of archive type, closed by #7121), and asks for nothing about whether archives load — only about what is reported when one does not.
Whether the .NET port shares the shape is worth checking rather than assuming: archive support landed there as #6077 (#6631, #6797), and this behaviour is a property of how refusals are logged rather than of the Python implementation specifically. If it does, this presumably wants the usual paired issue.
Description
An MCP skills index advertises N skills.
MCPSkillsSource.get_skills()can return fewer, and when it does there is nothing above DEBUG to say which ones went missing or why. A skill that failed to load is indistinguishable, from the caller's side, from a skill that was never published.Every refusal path in
_ArchiveEntryLoaderlogs atlogger.debug:name, or missingurlarchive_max_file_count,archive_max_size_bytesorarchive_max_uncompressed_size_bytesSKILL.mdSKILL.mdis not valid UTF-8SKILL.mdfrontmatternamedoes not match the advertised entry nameThe asymmetry inside that same class is what makes this look unintended rather than deliberate: a duplicate skill name logs at
logger.warning, and a failed archive read that is not an MCP-32002logs atlogger.warningbefore propagating. So the class already reaches for WARNING when it judges something notable — it just classifies "I refused to load a skill your index advertised" as routine.To reproduce
type: "archive".SKILL.mdfrontmatter anamethat differs from the index entry name — or let the package exceedarchive_max_file_count(default 20).MCPSkillsSource.get_skills()at the default log level.The skill is absent from the returned list. Nothing is logged above DEBUG.
Expected behavior
Refusing these packages is correct — a package whose internal name disagrees with the advertised one is a genuine integrity concern, and the extraction caps are DoS guards worth keeping. The refusal is not the problem; the silence is. A caller cannot act on a skill that vanished without knowing it vanished.
Raising the "refused a package the index advertised" paths to WARNING would resolve it, with no behavioral change and no API change. A single summary line at the end of discovery — advertised count versus loaded count, naming the difference — would cover every path above in one place and would arguably be the better shape, since it also catches whatever refusal paths get added later.
The distinction worth preserving: a malformed index entry is the server's fault and may reasonably stay quiet, but an entry that was well-formed enough to attempt and then failed extraction is a package the operator meant to ship.
Impact
Discovered in production. A toolbox advertising 16 skills logged
Successfully loaded 15 skills from MCP serverand served 15; the sixteenth was silently absent for an unknown period. The agent behaved as though that capability did not exist, which surfaces as degraded answers rather than as an error, so nothing draws attention to it.Three of the refusal paths are configurable and can be given headroom via the
archive_max_*constructor arguments. The frontmatter/index name mismatch is not configurable, so for that path there is no setting an operator can use to avoid the silence.Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.13.0
Python Version
No response
Additional Context
Additional context
The caps default to
archive_max_file_count=20,archive_max_size_bytes=1 MB,archive_max_uncompressed_size_bytes=1 MB. A package sitting just under the file cap crosses it on an ordinary content edit, so this is reachable without anything unusual happening — the failure arrives on a routine change and is silent when it does.This is a follow-up to #6088 (Support MCP skills of archive type, closed by #7121), and asks for nothing about whether archives load — only about what is reported when one does not.
Whether the .NET port shares the shape is worth checking rather than assuming: archive support landed there as #6077 (#6631, #6797), and this behaviour is a property of how refusals are logged rather than of the Python implementation specifically. If it does, this presumably wants the usual paired issue.